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 GroupEpoch, GroupId, LeafNodeLifetimePolicy, ProposalQueue, ProposalQueueError,
20 QueuedProposal, RatchetTreeExtension, StagedCommit, 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 errors::PskError,
33 psk::{load_psks, PskSecret, ResumptionPsk, ResumptionPskUsage},
34 EpochSecretsResult, JoinerSecret, KeySchedule, PreSharedKeyId, Psk, ResumptionPskSecret,
35 },
36 storage::{OpenMlsProvider, StorageProvider},
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};
50#[cfg(feature = "extensions-draft")]
51use crate::{
52 messages::proposals::AppDataUpdateProposal,
53 prelude::processing::{AppDataDictionaryUpdater, AppDataUpdates},
54 schedule::application_export_tree::ApplicationExportTree,
55};
56
57#[cfg(feature = "virtual-clients-draft")]
67#[derive(Debug)]
68struct VcLoaded {
69 epoch_id: EpochId,
70 emulation_leaf_index: LeafNodeIndex,
71 epoch_encryption_key: EpochEncryptionKey,
72 emulation_ciphersuite: openmls_traits::types::Ciphersuite,
73 generation: u32,
74 operation_secret: OperationSecret,
75 resolved_dictionary: AppDataDictionary,
79}
80
81pub(crate) mod external_commits;
82
83pub use external_commits::{
84 ExternalCommitBuilder, ExternalCommitBuilderError, VerifiedExternalCommitBuilder,
85};
86
87#[cfg(doc)]
88use super::MlsGroupJoinConfig;
89
90use super::{
91 branch::BranchInfo,
92 mls_auth_content::AuthenticatedContent,
93 staged_commit::{MemberStagedCommitState, StagedCommitState},
94 AddProposal, CreateCommitResult, GroupContextExtensionProposal, MlsGroup, MlsGroupState,
95 MlsMessageOut, PendingCommitState, Proposal, RemoveProposal, Sender,
96};
97
98#[cfg(feature = "virtual-clients-draft")]
99use super::HandshakeConfirmationData;
100
101#[derive(Debug)]
102struct ExternalCommitInfo {
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 bare_aad: Option<Vec<u8>>,
128
129 consume_proposal_store: bool,
132
133 lifetime_policy: LeafNodeLifetimePolicy,
135
136 foreign_resumption_psk: Option<(PreSharedKeyId, ResumptionPskSecret)>,
138}
139
140impl Default for Initial {
141 fn default() -> Self {
142 Initial {
143 consume_proposal_store: true,
144 force_self_update: false,
145 leaf_node_parameters: LeafNodeParameters::default(),
146 own_proposals: vec![],
147 external_commit_info: None,
148 lifetime_policy: LeafNodeLifetimePolicy::Verify,
149 bare_aad: None,
150 foreign_resumption_psk: None,
151 }
152 }
153}
154
155pub struct LoadedPsks {
157 own_proposals: Vec<Proposal>,
158 force_self_update: bool,
159 leaf_node_parameters: LeafNodeParameters,
160 external_commit_info: Option<ExternalCommitInfo>,
161
162 consume_proposal_store: bool,
165 psks: Vec<(PreSharedKeyId, Secret)>,
166
167 group_info_config: GroupInfoConfig,
169
170 lifetime_policy: LeafNodeLifetimePolicy,
172 bare_aad: Option<Vec<u8>>,
175
176 #[cfg(feature = "extensions-draft")]
177 app_data_dictionary_updates: Option<AppDataUpdates>,
178}
179
180#[derive(Debug)]
182pub struct Complete {
183 result: CreateCommitResult,
184 original_wire_format_policy: Option<WireFormatPolicy>,
186}
187
188#[derive(Debug)]
226pub struct CommitBuilder<'a, T, G: BorrowMut<MlsGroup> = &'a mut MlsGroup> {
227 group: G,
230
231 stage: T,
233
234 #[cfg(feature = "virtual-clients-draft")]
238 vc_loaded: Option<VcLoaded>,
239
240 #[cfg(feature = "virtual-clients-draft")]
243 vc_new_derivation_epoch: bool,
244
245 pd: PhantomData<&'a ()>,
246}
247
248impl<'a, T, G: BorrowMut<MlsGroup>> CommitBuilder<'a, T, G> {
249 pub(crate) fn replace_stage<NextStage>(
250 self,
251 next_stage: NextStage,
252 ) -> (T, CommitBuilder<'a, NextStage, G>) {
253 self.map_stage(|prev_stage| (prev_stage, next_stage))
254 }
255
256 pub(crate) fn into_stage<NextStage>(
257 self,
258 next_stage: NextStage,
259 ) -> CommitBuilder<'a, NextStage, G> {
260 self.replace_stage(next_stage).1
261 }
262
263 fn take_stage(self) -> (T, CommitBuilder<'a, (), G>) {
264 self.replace_stage(())
265 }
266
267 fn map_stage<NextStage, Aux, F: FnOnce(T) -> (Aux, NextStage)>(
268 self,
269 f: F,
270 ) -> (Aux, CommitBuilder<'a, NextStage, G>) {
271 let Self {
272 group,
273 stage,
274 #[cfg(feature = "virtual-clients-draft")]
275 vc_loaded,
276 #[cfg(feature = "virtual-clients-draft")]
277 vc_new_derivation_epoch,
278 pd: PhantomData,
279 } = self;
280
281 let (aux, stage) = f(stage);
282
283 (
284 aux,
285 CommitBuilder {
286 group,
287 stage,
288 #[cfg(feature = "virtual-clients-draft")]
289 vc_loaded,
290 #[cfg(feature = "virtual-clients-draft")]
291 vc_new_derivation_epoch,
292 pd: PhantomData,
293 },
294 )
295 }
296
297 #[cfg(feature = "fork-resolution")]
298 pub(crate) fn stage(&self) -> &T {
299 &self.stage
300 }
301
302 #[cfg(feature = "virtual-clients-draft")]
305 pub fn vc_epoch_id(&self) -> Option<&EpochId> {
306 self.vc_loaded.as_ref().map(|loaded| &loaded.epoch_id)
307 }
308}
309
310impl MlsGroup {
311 pub fn commit_builder(&mut self) -> CommitBuilder<'_, Initial> {
313 CommitBuilder::<'_, Initial, &mut MlsGroup>::new(self)
314 }
315}
316
317impl<'a> CommitBuilder<'a, Initial, &mut MlsGroup> {
319 pub fn consume_proposal_store(mut self, consume_proposal_store: bool) -> Self {
322 self.stage.consume_proposal_store = consume_proposal_store;
323 self
324 }
325
326 pub fn force_self_update(mut self, force_self_update: bool) -> Self {
328 self.stage.force_self_update = force_self_update;
329 self
330 }
331
332 pub fn propose_adds(mut self, key_packages: impl IntoIterator<Item = KeyPackage>) -> Self {
335 self.stage.own_proposals.extend(
336 key_packages
337 .into_iter()
338 .map(|key_package| Proposal::add(AddProposal { key_package })),
339 );
340 self
341 }
342
343 pub fn propose_removals(mut self, removed: impl IntoIterator<Item = LeafNodeIndex>) -> Self {
346 self.stage.own_proposals.extend(
347 removed
348 .into_iter()
349 .map(|removed| Proposal::remove(RemoveProposal { removed })),
350 );
351 self
352 }
353
354 pub fn propose_group_context_extensions(
357 mut self,
358 extensions: Extensions<GroupContext>,
359 ) -> Result<Self, CreateCommitError> {
360 let proposal = GroupContextExtensionProposal::new(extensions);
361 self.stage
362 .own_proposals
363 .push(Proposal::group_context_extensions(proposal));
364 Ok(self)
365 }
366 pub fn propose_psks(
374 mut self,
375 psk_ids: impl IntoIterator<Item = PreSharedKeyId>,
376 ) -> Result<Self, CreateCommitError> {
377 for psk_id in psk_ids {
378 if let Psk::Resumption(resumption_psk) = psk_id.psk() {
379 let usage = resumption_psk.usage();
380 if matches!(
381 usage,
382 ResumptionPskUsage::Branch | ResumptionPskUsage::Reinit
383 ) {
384 return Err(PskError::UsageMismatch {
385 allowed: vec![ResumptionPskUsage::Application],
386 got: usage,
387 }
388 .into());
389 }
390 }
391 self.stage
392 .own_proposals
393 .push(Proposal::psk(PreSharedKeyProposal::new(psk_id)));
394 }
395 Ok(self)
396 }
397
398 pub(crate) fn propose_psks_unchecked(
399 mut self,
400 psk_ids: impl IntoIterator<Item = PreSharedKeyId>,
401 ) -> Self {
402 self.stage.own_proposals.extend(
403 psk_ids
404 .into_iter()
405 .map(|psk_id| Proposal::psk(PreSharedKeyProposal::new(psk_id))),
406 );
407 self
408 }
409
410 pub(crate) fn branch(
427 mut self,
428 rand: &impl OpenMlsRand,
429 branch_info: &BranchInfo,
430 ) -> Result<Self, CreateCommitError> {
431 let psk_id = PreSharedKeyId::new(
433 branch_info.ciphersuite(),
434 rand,
435 Psk::Resumption(ResumptionPsk::new(
436 ResumptionPskUsage::Branch,
437 branch_info.group_id().clone(),
438 branch_info.epoch(),
439 )),
440 )
441 .map_err(LibraryError::unexpected_crypto_error)?;
442 self = self.propose_psks_unchecked([psk_id.clone()]);
443 self.stage.foreign_resumption_psk =
444 Some((psk_id, branch_info.resumption_psk_secret().clone()));
445 Ok(self)
446 }
447
448 pub(crate) fn reinit(
459 mut self,
460 rand: &impl OpenMlsRand,
461 old_group_id: GroupId,
462 old_group_epoch: GroupEpoch,
463 resumption_psk_secret: ResumptionPskSecret,
464 ) -> Result<Self, CreateCommitError> {
465 let psk_id = PreSharedKeyId::new(
469 self.group.ciphersuite(),
470 rand,
471 Psk::Resumption(ResumptionPsk::new(
472 ResumptionPskUsage::Reinit,
473 old_group_id,
474 old_group_epoch,
475 )),
476 )
477 .map_err(LibraryError::unexpected_crypto_error)?;
478 self = self.propose_psks_unchecked([psk_id.clone()]);
479 self.stage.foreign_resumption_psk = Some((psk_id, resumption_psk_secret));
480 Ok(self)
481 }
482
483 pub fn add_proposal(mut self, proposal: Proposal) -> Self {
486 self.stage.own_proposals.push(proposal);
487 self
488 }
489
490 pub fn add_proposals(mut self, proposals: impl IntoIterator<Item = Proposal>) -> Self {
492 self.stage.own_proposals.extend(proposals);
493 self
494 }
495}
496
497impl<'a, G: BorrowMut<MlsGroup>> CommitBuilder<'a, Initial, G> {
499 pub fn new(group: G) -> CommitBuilder<'a, Initial, G> {
501 let stage = Initial {
502 ..Default::default()
503 };
504 CommitBuilder {
505 group,
506 stage,
507 #[cfg(feature = "virtual-clients-draft")]
508 vc_loaded: None,
509 #[cfg(feature = "virtual-clients-draft")]
510 vc_new_derivation_epoch: false,
511 pd: PhantomData,
512 }
513 }
514
515 pub fn leaf_node_parameters(mut self, leaf_node_parameters: LeafNodeParameters) -> Self {
518 self.stage.leaf_node_parameters = leaf_node_parameters;
519 self
520 }
521
522 pub fn leaf_node_lifetime_policy(mut self, lifetime_policy: LeafNodeLifetimePolicy) -> Self {
529 self.stage.lifetime_policy = lifetime_policy;
530 self
531 }
532
533 #[cfg_attr(
537 feature = "extensions-draft",
538 doc = "",
539 doc = "When the GroupContext requires Safe AAD, this sets the tail only. The Safe AAD",
540 doc = "items staged on the group via [`MlsGroup::set_safe_aad`] are still prefixed to it,",
541 doc = "and are also reset when the commit is staged."
542 )]
543 pub fn with_aad(mut self, aad: Vec<u8>) -> Self {
544 self.stage.bare_aad = Some(aad);
545 self
546 }
547
548 #[cfg(feature = "virtual-clients-draft")]
603 #[openmls_traits::maybe_async]
604 pub async fn vc_emulation<Crypto: OpenMlsCrypto, Storage: StorageProvider>(
605 self,
606 crypto: &Crypto,
607 storage: &Storage,
608 emulation_group_id: &GroupId,
609 ) -> Result<Self, CreateCommitError> {
610 let epoch_id = require_newest_vc_derivation_epoch(storage, emulation_group_id).await?;
611 self.vc_emulation_internal(crypto, storage, epoch_id).await
612 }
613
614 #[cfg(all(feature = "virtual-clients-draft", any(test, feature = "test-utils")))]
623 #[openmls_traits::maybe_async]
624 pub async fn vc_emulation_at_epoch<Crypto: OpenMlsCrypto, Storage: StorageProvider>(
625 self,
626 crypto: &Crypto,
627 storage: &Storage,
628 epoch_id: EpochId,
629 ) -> Result<Self, CreateCommitError> {
630 self.vc_emulation_internal(crypto, storage, epoch_id).await
631 }
632
633 #[cfg(feature = "virtual-clients-draft")]
634 #[openmls_traits::maybe_async]
635 async fn vc_emulation_internal<Crypto: OpenMlsCrypto, Storage: StorageProvider>(
636 mut self,
637 crypto: &Crypto,
638 storage: &Storage,
639 epoch_id: EpochId,
640 ) -> Result<Self, CreateCommitError> {
641 let state: VcDerivationEpochState = storage
642 .vc_derivation_epoch_state(&epoch_id)
643 .await
644 .map_err(|e| {
645 log::error!("vc: load derivation epoch state in vc_emulation failed: {e:?}");
646 CreateCommitError::VirtualClientsError(VirtualClientsError::StorageError)
647 })?
648 .ok_or(VirtualClientsError::MissingDerivationEpochState)?;
649 let mut operation_tree: OperationSecretTree = storage
650 .vc_operation_tree(&epoch_id)
651 .await
652 .map_err(|e| {
653 log::error!("vc: load operation tree in vc_emulation failed: {e:?}");
654 CreateCommitError::VirtualClientsError(VirtualClientsError::StorageError)
655 })?
656 .ok_or(VirtualClientsError::MissingOperationTree)?;
657 let (emulation_leaf_index, epoch_encryption_key, emulation_ciphersuite) =
658 state.into_parts();
659
660 let own_leaf_index = self.group.borrow().own_leaf_index();
667 let is_external_commit = self.stage.external_commit_info.is_some();
668 let resolved_dictionary = check_vc_leaf_configuration(
669 &self.stage.leaf_node_parameters,
670 self.group.borrow(),
671 own_leaf_index,
672 is_external_commit,
673 )?;
674
675 let (generation, operation_secret) = operation_tree.next_operation_secret(
680 crypto,
681 emulation_ciphersuite,
682 &epoch_id,
683 emulation_leaf_index,
684 VirtualClientOperationType::LeafNode,
685 self.group.borrow().group_id().as_slice(),
686 )?;
687 storage
690 .write_vc_operation_tree(&epoch_id, &operation_tree)
691 .await
692 .map_err(|e| {
693 log::error!("vc: persist advanced operation tree failed: {e:?}");
694 CreateCommitError::VirtualClientsError(VirtualClientsError::StorageError)
695 })?;
696
697 self.vc_loaded = Some(VcLoaded {
698 epoch_id,
699 emulation_leaf_index,
700 epoch_encryption_key,
701 emulation_ciphersuite,
702 generation,
703 operation_secret,
704 resolved_dictionary,
705 });
706 Ok(self)
707 }
708
709 #[cfg(feature = "virtual-clients-draft")]
733 pub fn derivation_epoch(mut self, derivation_epoch: bool) -> Self {
734 self.vc_new_derivation_epoch = derivation_epoch;
735 self
736 }
737
738 #[openmls_traits::maybe_async]
740 pub async fn load_psks<Storage: StorageProvider>(
741 self,
742 storage: &'a Storage,
743 ) -> Result<CommitBuilder<'a, LoadedPsks, G>, CreateCommitError> {
744 let psk_ids: Vec<_> = self
745 .stage
746 .own_proposals
747 .iter()
748 .chain(
749 self.group
750 .borrow()
751 .proposal_store()
752 .proposals()
753 .map(|queued_proposal| queued_proposal.proposal()),
754 )
755 .filter_map(|proposal| match proposal {
756 Proposal::PreSharedKey(psk_proposal) => Some(psk_proposal.clone().into_psk_id()),
757 _ => None,
758 })
759 .collect();
760
761 let psks = load_psks(
763 storage,
764 &self.group.borrow().resumption_psk_store,
765 Some(self.group.borrow().group_id()),
766 self.stage
767 .foreign_resumption_psk
768 .as_ref()
769 .map(|(psk_id, secret)| (psk_id, secret.clone())),
770 &psk_ids,
771 )
772 .await?
773 .into_iter()
774 .map(|(psk_id_ref, key)| (psk_id_ref.clone(), key))
775 .collect();
776
777 let use_ratchet_tree_extension = self
779 .group
780 .borrow()
781 .configuration()
782 .use_ratchet_tree_extension;
783
784 let group_info_config = GroupInfoConfig {
785 use_ratchet_tree_extension,
786 create_group_info: use_ratchet_tree_extension,
787 other_extensions: vec![],
788 };
789
790 Ok(self
791 .map_stage(|stage| {
792 (
793 (),
794 LoadedPsks {
795 own_proposals: stage.own_proposals,
796 psks,
797 force_self_update: stage.force_self_update,
798 leaf_node_parameters: stage.leaf_node_parameters,
799 consume_proposal_store: stage.consume_proposal_store,
800 bare_aad: stage.bare_aad,
801 group_info_config,
802 external_commit_info: stage.external_commit_info,
803 lifetime_policy: stage.lifetime_policy,
804 #[cfg(feature = "extensions-draft")]
805 app_data_dictionary_updates: None,
806 },
807 )
808 })
809 .1)
810 }
811}
812
813impl<'a, G: BorrowMut<MlsGroup>> CommitBuilder<'a, LoadedPsks, G> {
814 pub fn create_group_info(mut self, create_group_info: bool) -> Self {
817 self.stage.group_info_config.create_group_info = create_group_info;
818 self
819 }
820
821 pub fn use_ratchet_tree_extension(mut self, use_ratchet_tree_extension: bool) -> Self {
824 if use_ratchet_tree_extension {
825 self.stage.group_info_config.create_group_info = true;
826 }
827 self.stage.group_info_config.use_ratchet_tree_extension = use_ratchet_tree_extension;
828 self
829 }
830
831 pub fn create_group_info_with_extensions(
836 mut self,
837 extensions: impl IntoIterator<Item = Extension>,
838 ) -> Result<Self, InvalidExtensionError> {
839 self.stage.group_info_config.create_group_info = true;
840 self.stage.group_info_config.other_extensions = extensions
841 .into_iter()
842 .map(|extension| {
843 if extension.as_ratchet_tree_extension().is_ok()
844 || extension.as_external_pub_extension().is_ok()
845 {
846 Err(InvalidExtensionError::CannotAddDirectlyToGroupInfo)
847 } else {
848 Ok(extension)
849 }
850 })
851 .collect::<Result<Vec<_>, _>>()?;
852
853 Ok(self)
854 }
855
856 pub fn build<S: Signer>(
864 self,
865 rand: &impl OpenMlsRand,
866 crypto: &impl OpenMlsCrypto,
867 signer: &S,
868 f: impl FnMut(&QueuedProposal) -> bool,
869 ) -> Result<CommitBuilder<'a, Complete, G>, CreateCommitError> {
870 self.build_internal(rand, crypto, signer, None::<NewSignerBundle<'_, S>>, f)
871 }
872
873 pub fn build_with_new_signer<S: Signer>(
893 self,
894 rand: &impl OpenMlsRand,
895 crypto: &impl OpenMlsCrypto,
896 old_signer: &impl Signer,
897 new_signer: NewSignerBundle<'_, S>,
898 f: impl FnMut(&QueuedProposal) -> bool,
899 ) -> Result<CommitBuilder<'a, Complete, G>, CreateCommitError> {
900 if self.stage.external_commit_info.is_some() {
904 return Err(CreateCommitError::ExternalCommitWithNewSigner);
905 }
906 self.build_internal(rand, crypto, old_signer, Some(new_signer), f)
907 }
908
909 fn build_internal<S: Signer>(
910 self,
911 rand: &impl OpenMlsRand,
912 crypto: &impl OpenMlsCrypto,
913 old_signer: &impl Signer,
914 new_signer: Option<NewSignerBundle<'_, S>>,
915 f: impl FnMut(&QueuedProposal) -> bool,
916 ) -> Result<CommitBuilder<'a, Complete, G>, CreateCommitError> {
917 #[cfg_attr(not(feature = "virtual-clients-draft"), allow(unused_mut))]
918 let (mut cur_stage, mut builder) = self.take_stage();
919
920 let GroupInfoConfig {
922 create_group_info,
923 use_ratchet_tree_extension,
924 other_extensions,
925 } = cur_stage.group_info_config;
926 let lifetime_policy = cur_stage.lifetime_policy;
927
928 #[cfg(feature = "virtual-clients-draft")]
933 if builder.vc_new_derivation_epoch {
934 stage_vc_new_derivation_epoch(builder.group.borrow_mut())?;
935 }
936
937 let group = builder.group.borrow();
938
939 #[cfg(feature = "virtual-clients-draft")]
943 let marks_new_vc_derivation_epoch = staged_vc_commit_data(group)?
944 .is_some_and(|commit_data| commit_data.creates_derivation_epoch());
945 let ciphersuite = group.ciphersuite();
946 let own_leaf_index = group.own_leaf_index();
947 let (sender, is_external_commit) = match cur_stage.external_commit_info {
948 None => (Sender::build_member(own_leaf_index), false),
949 Some(_) => (Sender::NewMemberCommit, true),
950 };
951 let psks = cur_stage.psks;
952
953 if let Some(ExternalCommitInfo { credential, .. }) = &cur_stage.external_commit_info {
959 if let Some(params_credential) = cur_stage.leaf_node_parameters.credential_with_key() {
960 if params_credential != credential {
961 return Err(CreateCommitError::ExternalCommitCredentialMismatch);
962 }
963 }
964 cur_stage
965 .leaf_node_parameters
966 .set_credential_with_key(credential.clone());
967 }
968
969 let new_signer = match new_signer {
974 Some(NewSignerBundle {
975 signer,
976 credential_with_key,
977 }) => {
978 if ciphersuite.signature_algorithm() != signer.signature_scheme() {
979 return Err(CreateCommitError::InvalidSignerCiphersuite);
980 }
981 if let Some(params_credential) =
982 cur_stage.leaf_node_parameters.credential_with_key()
983 {
984 if params_credential != &credential_with_key {
985 return Err(CreateCommitError::InvalidLeafNodeParameters);
986 }
987 }
988 cur_stage
989 .leaf_node_parameters
990 .set_credential_with_key(credential_with_key);
991 Some(signer)
992 }
993 None => None,
994 };
995
996 let own_proposals: Vec<_> = cur_stage
999 .own_proposals
1000 .into_iter()
1001 .map(|proposal| {
1002 QueuedProposal::from_proposal_and_sender(ciphersuite, crypto, proposal, &sender)
1003 })
1004 .collect::<Result<_, _>>()?;
1005
1006 let group_proposal_store_queue = group
1009 .pending_proposals()
1010 .filter(|_| cur_stage.consume_proposal_store)
1011 .cloned();
1012
1013 let proposal_queue = group_proposal_store_queue.chain(own_proposals).filter(f);
1017
1018 let (proposal_queue, contains_own_updates) =
1019 ProposalQueue::filter_proposals(proposal_queue, group.own_leaf_index).map_err(|e| {
1020 match e {
1021 ProposalQueueError::LibraryError(e) => e.into(),
1022 ProposalQueueError::ProposalNotFound => CreateCommitError::MissingProposal,
1023 ProposalQueueError::UpdateFromExternalSender
1024 | ProposalQueueError::SelfRemoveFromNonMember => {
1025 CreateCommitError::WrongProposalSenderType
1026 }
1027 }
1028 })?;
1029
1030 group
1035 .public_group
1036 .validate_proposal_type_support(&proposal_queue)?;
1037 let path_leaf_signature_key = cur_stage
1042 .leaf_node_parameters
1043 .credential_with_key()
1044 .map(|credential_with_key| &credential_with_key.signature_key);
1045 group.public_group.validate_key_uniqueness(
1046 &proposal_queue,
1047 None,
1048 &sender,
1049 path_leaf_signature_key,
1050 )?;
1051 group
1053 .public_group
1054 .validate_add_proposals(&proposal_queue, lifetime_policy)?;
1055 group.public_group.validate_capabilities(&proposal_queue)?;
1058 group
1061 .public_group
1062 .validate_remove_proposals(&proposal_queue)?;
1063 group
1066 .public_group
1067 .validate_pre_shared_key_proposals(&proposal_queue)?;
1068 group
1071 .public_group
1072 .validate_reinit_proposals(&proposal_queue)?;
1073 group
1078 .public_group
1079 .validate_update_proposals(&proposal_queue, own_leaf_index)?;
1080
1081 group
1084 .public_group
1085 .validate_group_context_extensions_proposal(&proposal_queue)?;
1086
1087 #[cfg(feature = "extensions-draft")]
1088 group
1089 .public_group
1090 .validate_app_data_update_proposals_and_group_context(&proposal_queue)?;
1091
1092 if is_external_commit {
1093 group
1094 .public_group
1095 .validate_external_commit(&proposal_queue)?;
1096 }
1097
1098 let proposal_reference_list = proposal_queue.commit_list();
1099
1100 let mut diff = group.public_group.empty_diff();
1102
1103 #[cfg(feature = "extensions-draft")]
1105 let apply_proposals_values = diff.apply_proposals_with_app_data_updates(
1106 &proposal_queue,
1107 own_leaf_index,
1108 cur_stage.app_data_dictionary_updates,
1109 )?;
1110 #[cfg(not(feature = "extensions-draft"))]
1111 let apply_proposals_values = diff.apply_proposals(&proposal_queue, own_leaf_index)?;
1112 if apply_proposals_values.self_removed && !is_external_commit {
1113 return Err(CreateCommitError::CannotRemoveSelf);
1114 }
1115
1116 #[cfg(feature = "virtual-clients-draft")]
1124 let vc_loaded = builder.vc_loaded.take();
1125 #[cfg(feature = "virtual-clients-draft")]
1126 let own_update_override = if let Some(loaded) = vc_loaded.as_ref() {
1127 let external_init_secret =
1137 is_external_commit.then(|| group.group_epoch_secrets().init_secret());
1138 Some(apply_vc_emulation(
1139 loaded,
1140 &mut cur_stage.leaf_node_parameters,
1141 loaded.resolved_dictionary.clone(),
1142 crypto,
1143 ciphersuite,
1144 group.group_id(),
1145 external_init_secret,
1146 )?)
1147 } else {
1148 None
1149 };
1150 #[cfg(not(feature = "virtual-clients-draft"))]
1151 let own_update_override: Option<crate::treesync::diff::OwnUpdatePathOverride> = None;
1152
1153 let path_computation_result =
1156 if apply_proposals_values.path_required
1158 || contains_own_updates
1159 || cur_stage.force_self_update
1160 || !cur_stage.leaf_node_parameters.is_empty()
1161 || new_signer.is_some()
1162 {
1163 let commit_type = if is_external_commit {
1164 CommitType::External
1165 } else {
1166 CommitType::Member
1167 };
1168 let mut leaf_node_constraints = group.public_group.leaf_node_constraints();
1171 if let Some(extensions) = &apply_proposals_values.extensions {
1172 leaf_node_constraints.add_group_context_extensions(extensions);
1173 }
1174 match new_signer {
1178 Some(new_signer) => diff.compute_path(
1179 rand,
1180 crypto,
1181 own_leaf_index,
1182 apply_proposals_values.exclusion_list(),
1183 &commit_type,
1184 &cur_stage.leaf_node_parameters,
1185 new_signer,
1186 apply_proposals_values.extensions.clone(),
1187 leaf_node_constraints,
1188 own_update_override,
1189 )?,
1190 None => diff.compute_path(
1191 rand,
1192 crypto,
1193 own_leaf_index,
1194 apply_proposals_values.exclusion_list(),
1195 &commit_type,
1196 &cur_stage.leaf_node_parameters,
1197 old_signer,
1198 apply_proposals_values.extensions.clone(),
1199 leaf_node_constraints,
1200 own_update_override,
1201 )?,
1202 }
1203 } else {
1204 diff.update_group_context(crypto, apply_proposals_values.extensions.clone())?;
1207 PathComputationResult::default()
1208 };
1209
1210 let update_path_leaf_node = path_computation_result
1211 .encrypted_path
1212 .as_ref()
1213 .map(|path| path.leaf_node().clone());
1214
1215 let commit = Commit {
1217 proposals: proposal_reference_list,
1218 path: path_computation_result.encrypted_path,
1219 };
1220
1221 let wire_format = if cur_stage.external_commit_info.is_some() {
1222 WireFormat::PublicMessage
1223 } else {
1224 group.outgoing_wire_format()
1225 };
1226
1227 let outgoing_aad = if let Some(aad) = cur_stage.bare_aad {
1228 #[cfg(feature = "extensions-draft")]
1235 if group.context().safe_aad_required() {
1236 crate::framing::safe_aad::assemble_authenticated_data(&group.safe_aad, &aad)
1237 .map_err(|_| LibraryError::custom("SafeAad serialization failed"))?
1238 } else {
1239 aad
1240 }
1241
1242 #[cfg(not(feature = "extensions-draft"))]
1243 aad
1244 } else {
1245 group.outgoing_authenticated_data()?
1246 };
1247
1248 let framing_parameters = FramingParameters::new(&outgoing_aad, wire_format);
1249
1250 let mut authenticated_content = AuthenticatedContent::commit(
1252 framing_parameters,
1253 sender,
1254 commit,
1255 group.public_group.group_context(),
1256 old_signer,
1257 )?;
1258
1259 diff.update_confirmed_transcript_hash(crypto, &authenticated_content)?;
1261
1262 let serialized_provisional_group_context = diff
1263 .group_context()
1264 .tls_serialize_detached()
1265 .map_err(LibraryError::missing_bound_check)?;
1266
1267 let joiner_secret = JoinerSecret::new(
1268 crypto,
1269 ciphersuite,
1270 path_computation_result.commit_secret,
1271 group.group_epoch_secrets().init_secret(),
1272 &serialized_provisional_group_context,
1273 )
1274 .map_err(LibraryError::unexpected_crypto_error)?;
1275
1276 let psk_secret = PskSecret::new(crypto, ciphersuite, psks)?;
1278
1279 let mut key_schedule = KeySchedule::init(ciphersuite, crypto, &joiner_secret, psk_secret)?;
1281
1282 let serialized_provisional_group_context = diff
1283 .group_context()
1284 .tls_serialize_detached()
1285 .map_err(LibraryError::missing_bound_check)?;
1286
1287 let welcome_secret = key_schedule
1288 .welcome(crypto, ciphersuite)
1289 .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
1290 key_schedule
1291 .add_context(crypto, &serialized_provisional_group_context)
1292 .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
1293 let EpochSecretsResult {
1294 epoch_secrets: provisional_epoch_secrets,
1295 #[cfg(feature = "extensions-draft")]
1296 application_exporter,
1297 } = key_schedule
1298 .epoch_secrets(crypto, ciphersuite)
1299 .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
1300
1301 let confirmation_tag = provisional_epoch_secrets
1303 .confirmation_key()
1304 .tag(
1305 crypto,
1306 ciphersuite,
1307 diff.group_context().confirmed_transcript_hash(),
1308 )
1309 .map_err(LibraryError::unexpected_crypto_error)?;
1310
1311 authenticated_content.set_confirmation_tag(confirmation_tag.clone());
1313
1314 diff.update_interim_transcript_hash(ciphersuite, crypto, confirmation_tag.clone())?;
1315
1316 let needs_welcome = !apply_proposals_values.invitation_list.is_empty();
1318
1319 let needs_group_info = needs_welcome || create_group_info;
1323
1324 let (welcome_option, group_info) = if !needs_group_info {
1325 (None, None)
1326 } else {
1327 let mut extensions_list = vec![];
1329 if use_ratchet_tree_extension {
1330 extensions_list.push(Extension::RatchetTree(RatchetTreeExtension::new(
1331 diff.export_ratchet_tree(),
1332 )));
1333 };
1334 extensions_list.extend(other_extensions);
1336
1337 let mut extensions = Extensions::from_vec(extensions_list)?;
1338
1339 let welcome_option = needs_welcome
1340 .then(|| -> Result<_, CreateCommitError> {
1341 let group_info_tbs = {
1342 GroupInfoTBS::new(
1343 diff.group_context().clone(),
1344 extensions.clone(),
1345 confirmation_tag.clone(),
1346 own_leaf_index,
1347 )?
1348 };
1349 let group_info = match new_signer {
1353 Some(new_signer) => group_info_tbs.sign(new_signer)?,
1354 None => group_info_tbs.sign(old_signer)?,
1355 };
1356
1357 let (welcome_key, welcome_nonce) = welcome_secret
1359 .derive_welcome_key_nonce(crypto, ciphersuite)
1360 .map_err(LibraryError::unexpected_crypto_error)?;
1361 let encrypted_group_info = welcome_key
1362 .aead_seal(
1363 crypto,
1364 group_info
1365 .tls_serialize_detached()
1366 .map_err(LibraryError::missing_bound_check)?
1367 .as_slice(),
1368 &[],
1369 &welcome_nonce,
1370 )
1371 .map_err(LibraryError::unexpected_crypto_error)?;
1372
1373 let encrypted_secrets = diff.encrypt_group_secrets(
1376 &joiner_secret,
1377 apply_proposals_values.invitation_list,
1378 path_computation_result.plain_path.as_deref(),
1379 &apply_proposals_values.presharedkeys,
1380 &encrypted_group_info,
1381 crypto,
1382 own_leaf_index,
1383 )?;
1384
1385 let welcome =
1387 Welcome::new(ciphersuite, encrypted_secrets, encrypted_group_info);
1388 Ok(welcome)
1389 })
1390 .transpose()?;
1391
1392 let exported_group_info = create_group_info
1395 .then(|| -> Result<_, CreateCommitError> {
1396 let external_pub = provisional_epoch_secrets
1397 .external_secret()
1398 .derive_external_keypair(crypto, ciphersuite)
1399 .map_err(LibraryError::unexpected_crypto_error)?
1400 .public;
1401
1402 let external_pub_extension =
1403 Extension::ExternalPub(ExternalPubExtension::new(external_pub.into()));
1404 extensions.add(external_pub_extension)?;
1405 let group_info_tbs = {
1406 GroupInfoTBS::new(
1407 diff.group_context().clone(),
1408 extensions,
1409 confirmation_tag.clone(),
1410 own_leaf_index,
1411 )?
1412 };
1413 match new_signer {
1418 Some(new_signer) => Ok(group_info_tbs.sign(new_signer)?),
1419 None => Ok(group_info_tbs.sign(old_signer)?),
1420 }
1421 })
1422 .transpose()?;
1423
1424 (welcome_option, exported_group_info)
1425 };
1426
1427 let (provisional_group_epoch_secrets, provisional_message_secrets) =
1428 provisional_epoch_secrets.split_secrets(
1429 serialized_provisional_group_context,
1430 diff.tree_size(),
1431 own_leaf_index,
1432 );
1433
1434 #[cfg(feature = "extensions-draft")]
1435 let application_export_tree = ApplicationExportTree::new(application_exporter);
1436 let staged_commit_state = MemberStagedCommitState::new(
1437 provisional_group_epoch_secrets,
1438 provisional_message_secrets,
1439 diff.into_staged_diff(crypto, ciphersuite)?,
1440 path_computation_result.new_keypairs,
1441 None,
1444 update_path_leaf_node,
1445 #[cfg(feature = "extensions-draft")]
1446 application_export_tree,
1447 #[cfg(feature = "virtual-clients-draft")]
1451 None,
1452 );
1453 #[cfg_attr(not(feature = "virtual-clients-draft"), allow(unused_mut))]
1454 let mut staged_commit = StagedCommit::new(
1455 proposal_queue,
1456 StagedCommitState::GroupMember(Box::new(staged_commit_state)),
1457 #[cfg(feature = "virtual-clients-draft")]
1458 vc_loaded.as_ref().map(|loaded| loaded.epoch_id.clone()),
1459 );
1460 #[cfg(feature = "virtual-clients-draft")]
1461 {
1462 staged_commit.marks_new_vc_derivation_epoch = marks_new_vc_derivation_epoch;
1463 }
1464
1465 Ok(builder.into_stage(Complete {
1466 result: CreateCommitResult {
1467 commit: authenticated_content,
1468 welcome_option,
1469 staged_commit,
1470 group_info: group_info.filter(|_| create_group_info),
1471 },
1472 original_wire_format_policy: cur_stage
1473 .external_commit_info
1474 .as_ref()
1475 .map(|info| info.wire_format_policy),
1476 }))
1477 }
1478
1479 #[cfg(feature = "extensions-draft")]
1484 pub fn app_data_dictionary_updater(&self) -> AppDataDictionaryUpdater<'_> {
1485 AppDataDictionaryUpdater::new(self.group.borrow().context().app_data_dict())
1486 }
1487
1488 #[cfg(feature = "extensions-draft")]
1490 pub fn with_app_data_dictionary_updates(
1491 &mut self,
1492 app_data_dictionary_updates: Option<AppDataUpdates>,
1493 ) {
1494 self.stage.app_data_dictionary_updates = app_data_dictionary_updates;
1495 }
1496
1497 #[cfg(feature = "extensions-draft")]
1499 pub fn app_data_update_proposals(&self) -> impl Iterator<Item = &AppDataUpdateProposal> {
1500 let proposal_store_proposals = self
1501 .group
1502 .borrow()
1503 .proposal_store()
1504 .proposals()
1505 .map(|queued_proposal| queued_proposal.proposal());
1506
1507 let all_proposals = proposal_store_proposals.chain(self.stage.own_proposals.iter());
1509
1510 let mut app_data_update_proposals: Vec<&AppDataUpdateProposal> = all_proposals
1512 .filter_map(|proposal| match proposal {
1513 Proposal::AppDataUpdate(proposal) => Some(proposal.as_ref()),
1514 _ => None,
1515 })
1516 .collect();
1517
1518 app_data_update_proposals.sort_by_key(|prop| prop.component_id());
1519 app_data_update_proposals.into_iter()
1520 }
1521}
1522
1523impl CommitBuilder<'_, Complete, &mut MlsGroup> {
1525 #[cfg(test)]
1526 pub(crate) fn commit_result(self) -> CreateCommitResult {
1527 self.stage.result
1528 }
1529
1530 #[openmls_traits::maybe_async]
1532 pub async fn stage_commit<Provider: OpenMlsProvider>(
1533 self,
1534 provider: &Provider,
1535 ) -> Result<CommitMessageBundle, CommitBuilderStageError<Provider::StorageError>> {
1536 let Self {
1537 group,
1538 stage:
1539 Complete {
1540 result: create_commit_result,
1541 original_wire_format_policy: _,
1542 },
1543 ..
1544 } = self;
1545
1546 group.group_state = MlsGroupState::PendingCommit(Box::new(PendingCommitState::Member(
1549 create_commit_result.staged_commit,
1550 )));
1551
1552 provider
1553 .storage()
1554 .write_group_state(group.group_id(), &group.group_state)
1555 .await
1556 .map_err(CommitBuilderStageError::KeyStoreError)?;
1557
1558 group.reset_aad();
1559
1560 let framing = group
1566 .content_to_mls_message(create_commit_result.commit, provider)
1567 .await?;
1568
1569 Ok(CommitMessageBundle {
1570 version: group.version(),
1571 commit: framing.message,
1572 welcome: create_commit_result.welcome_option,
1573 group_info: create_commit_result.group_info,
1574 #[cfg(feature = "virtual-clients-draft")]
1575 confirmation: framing.confirmation,
1576 })
1577 }
1578}
1579
1580#[cfg(feature = "virtual-clients-draft")]
1586fn staged_vc_commit_data(
1587 group: &MlsGroup,
1588) -> Result<Option<VirtualClientCommitData>, CreateCommitError> {
1589 if !group.is_emulation_group() || !group.context().safe_aad_required() {
1590 return Ok(None);
1591 }
1592 Ok(VirtualClientCommitData::from_safe_aad(&group.safe_aad)?)
1593}
1594
1595#[cfg(feature = "virtual-clients-draft")]
1602fn stage_vc_new_derivation_epoch(group: &mut MlsGroup) -> Result<(), CreateCommitError> {
1603 if !group.is_emulation_group() {
1604 return Err(CreateCommitError::NewDerivationEpochOutsideEmulationGroup);
1605 }
1606 if !group.context().safe_aad_required() {
1607 return Err(CreateCommitError::NewDerivationEpochWithoutSafeAad);
1608 }
1609
1610 let mut commit_data = staged_vc_commit_data(group)?
1611 .map_or_else(|| VirtualClientCommitData::new(Vec::new()), Ok)?;
1612 commit_data.require_new_derivation_epoch();
1613 group.safe_aad.upsert(commit_data.to_safe_aad_item()?);
1614 Ok(())
1615}
1616
1617#[cfg(feature = "virtual-clients-draft")]
1629fn apply_vc_emulation(
1630 loaded: &VcLoaded,
1631 leaf_node_parameters: &mut LeafNodeParameters,
1632 resolved_dictionary: AppDataDictionary,
1633 crypto: &impl OpenMlsCrypto,
1634 group_ciphersuite: openmls_traits::types::Ciphersuite,
1635 group_id: &crate::prelude::GroupId,
1636 external_init_secret: Option<&crate::schedule::InitSecret>,
1637) -> Result<crate::treesync::diff::OwnUpdatePathOverride, CreateCommitError> {
1638 let target_operation_secret = loaded.operation_secret.derive_target_operation_secret(
1639 crypto,
1640 group_ciphersuite,
1641 group_id,
1642 )?;
1643 let path_secret = target_operation_secret
1644 .derive_path_generation_secret(crypto, group_ciphersuite)?
1645 .into();
1646 let leaf_encryption_keypair = target_operation_secret
1647 .derive_encryption_key_secret(crypto, group_ciphersuite)?
1648 .generate_encryption_key_pair(crypto, group_ciphersuite)?;
1649 drop(target_operation_secret);
1650
1651 let leaf_encryption_key = leaf_encryption_keypair
1654 .public_key()
1655 .tls_serialize_detached()
1656 .map_err(VirtualClientsError::from)?;
1657 let tbe = DerivationInfoTbe::LeafNode {
1661 leaf_index: loaded.emulation_leaf_index,
1662 generation: loaded.generation,
1663 external_init_secret: external_init_secret
1664 .map(|init_secret| ExternalInitSecret::from_slice(init_secret.as_slice())),
1665 };
1666 let derivation_info = DerivationInfo::encrypt(
1667 crypto,
1668 loaded.emulation_ciphersuite,
1669 &loaded.epoch_encryption_key,
1670 loaded.epoch_id.clone(),
1671 &leaf_encryption_key,
1672 &tbe,
1673 )?;
1674 let derivation_info_bytes = derivation_info
1675 .tls_serialize_detached()
1676 .map_err(VirtualClientsError::from)?;
1677
1678 inject_vc_derivation_info(
1679 leaf_node_parameters,
1680 resolved_dictionary,
1681 derivation_info_bytes,
1682 )?;
1683
1684 Ok(crate::treesync::diff::OwnUpdatePathOverride {
1685 path_secret,
1686 leaf_encryption_keypair,
1687 })
1688}
1689
1690#[cfg(feature = "virtual-clients-draft")]
1702fn check_vc_leaf_configuration(
1703 leaf_node_parameters: &LeafNodeParameters,
1704 group: &MlsGroup,
1705 own_leaf_index: LeafNodeIndex,
1706 is_external_commit: bool,
1707) -> Result<AppDataDictionary, CreateCommitError> {
1708 let current_leaf = if is_external_commit {
1709 None
1710 } else {
1711 Some(group.public_group().leaf(own_leaf_index).ok_or_else(|| {
1712 LibraryError::custom("Couldn't find own leaf for VC capability check")
1713 })?)
1714 };
1715
1716 crate::components::vc_derivation_info::resolve_vc_leaf_dictionary(
1717 leaf_node_parameters.capabilities(),
1718 leaf_node_parameters.extensions(),
1719 current_leaf,
1720 )
1721 .map_err(CreateCommitError::VirtualClientsError)
1722}
1723
1724#[cfg(feature = "virtual-clients-draft")]
1729fn inject_vc_derivation_info(
1730 leaf_node_parameters: &mut LeafNodeParameters,
1731 resolved_dictionary: AppDataDictionary,
1732 derivation_info_bytes: Vec<u8>,
1733) -> Result<(), CreateCommitError> {
1734 let extensions = crate::components::vc_derivation_info::merge_vc_derivation_info(
1735 leaf_node_parameters.extensions(),
1736 resolved_dictionary,
1737 derivation_info_bytes,
1738 )?;
1739 leaf_node_parameters.set_extensions(extensions);
1740 Ok(())
1741}
1742
1743#[derive(Debug, Clone)]
1746pub struct CommitMessageBundle {
1747 version: ProtocolVersion,
1748 commit: MlsMessageOut,
1749 welcome: Option<Welcome>,
1750 group_info: Option<GroupInfo>,
1751 #[cfg(feature = "virtual-clients-draft")]
1754 confirmation: Option<HandshakeConfirmationData>,
1755}
1756
1757pub struct WelcomeCommitMessages {
1762 pub commit: MlsMessageOut,
1764
1765 pub welcome: MlsMessageOut,
1767
1768 pub group_info: Option<MlsMessageOut>,
1770}
1771
1772impl TryFrom<CommitMessageBundle> for WelcomeCommitMessages {
1773 type Error = LibraryError;
1774
1775 fn try_from(value: CommitMessageBundle) -> Result<Self, Self::Error> {
1776 let (commit, welcome_opt, group_info) = value.into_messages();
1777 Ok(Self {
1778 commit,
1779 welcome: welcome_opt.ok_or_else(|| {
1780 LibraryError::custom(
1781 "WelcomeCommitMessages must only be used with commits that produce a welcome.",
1782 )
1783 })?,
1784 group_info,
1785 })
1786 }
1787}
1788
1789#[cfg(test)]
1790impl CommitMessageBundle {
1791 pub fn new(
1792 version: ProtocolVersion,
1793 commit: MlsMessageOut,
1794 welcome: Option<Welcome>,
1795 group_info: Option<GroupInfo>,
1796 ) -> Self {
1797 Self {
1798 version,
1799 commit,
1800 welcome,
1801 group_info,
1802 #[cfg(feature = "virtual-clients-draft")]
1803 confirmation: None,
1804 }
1805 }
1806}
1807
1808impl CommitMessageBundle {
1809 pub fn commit(&self) -> &MlsMessageOut {
1813 &self.commit
1814 }
1815
1816 pub fn welcome(&self) -> Option<&Welcome> {
1819 self.welcome.as_ref()
1820 }
1821
1822 pub fn to_welcome_msg(&self) -> Option<MlsMessageOut> {
1825 self.welcome
1826 .as_ref()
1827 .map(|welcome| MlsMessageOut::from_welcome(welcome.clone(), self.version))
1828 }
1829
1830 pub fn group_info(&self) -> Option<&GroupInfo> {
1834 self.group_info.as_ref()
1835 }
1836
1837 #[cfg(feature = "virtual-clients-draft")]
1845 pub fn confirmation(&self) -> Option<&HandshakeConfirmationData> {
1846 self.confirmation.as_ref()
1847 }
1848
1849 #[cfg(feature = "virtual-clients-draft")]
1855 pub fn take_confirmation(&mut self) -> Option<HandshakeConfirmationData> {
1856 self.confirmation.take()
1857 }
1858
1859 pub fn contents(&self) -> (&MlsMessageOut, Option<&Welcome>, Option<&GroupInfo>) {
1862 (
1863 &self.commit,
1864 self.welcome.as_ref(),
1865 self.group_info.as_ref(),
1866 )
1867 }
1868
1869 pub fn into_commit(self) -> MlsMessageOut {
1873 self.commit
1874 }
1875
1876 pub fn into_welcome(self) -> Option<Welcome> {
1880 self.welcome
1881 }
1882
1883 pub fn into_welcome_msg(self) -> Option<MlsMessageOut> {
1886 self.welcome
1887 .map(|welcome| MlsMessageOut::from_welcome(welcome, self.version))
1888 }
1889
1890 pub fn into_group_info(self) -> Option<GroupInfo> {
1895 self.group_info
1896 }
1897
1898 pub fn into_group_info_msg(self) -> Option<MlsMessageOut> {
1900 self.group_info.map(|group_info| group_info.into())
1901 }
1902
1903 pub fn into_contents(self) -> (MlsMessageOut, Option<Welcome>, Option<GroupInfo>) {
1906 (self.commit, self.welcome, self.group_info)
1907 }
1908
1909 pub fn into_messages(self) -> (MlsMessageOut, Option<MlsMessageOut>, Option<MlsMessageOut>) {
1912 (
1913 self.commit,
1914 self.welcome
1915 .map(|welcome| MlsMessageOut::from_welcome(welcome, self.version)),
1916 self.group_info.map(|group_info| group_info.into()),
1917 )
1918 }
1919}
1920
1921impl IntoIterator for CommitMessageBundle {
1922 type Item = MlsMessageOut;
1923
1924 type IntoIter = core::iter::Chain<
1925 core::iter::Chain<
1926 core::option::IntoIter<MlsMessageOut>,
1927 core::option::IntoIter<MlsMessageOut>,
1928 >,
1929 core::option::IntoIter<MlsMessageOut>,
1930 >;
1931
1932 fn into_iter(self) -> Self::IntoIter {
1933 let welcome = self.to_welcome_msg();
1934 let group_info = self.group_info.map(|group_info| group_info.into());
1935
1936 Some(self.commit)
1937 .into_iter()
1938 .chain(welcome)
1939 .chain(group_info)
1940 }
1941}
1942
1943#[cfg(test)]
1944mod tests {
1945 use crate::{
1946 group::{
1947 mls_group::tests_and_kats::utils::{setup_alice_bob_group, setup_client},
1948 CreateCommitError, MlsGroup, ProposalValidationError,
1949 },
1950 schedule::errors::PskError,
1951 };
1952
1953 #[openmls_test::openmls_test]
1961 fn subgroup_branch_psk_rejected_outside_initial_commit() {
1962 let alice_provider = &Provider::default();
1963 let bob_provider = &Provider::default();
1964 let parent_provider = &Provider::default();
1965
1966 let (mut alice_group, alice_signer, _bob_group, _bob_signer, _alice_cwk, _bob_cwk) =
1967 setup_alice_bob_group(ciphersuite, alice_provider, bob_provider);
1968
1969 let (parent_cwk, _parent_kpb, parent_signer, _parent_pk) =
1973 setup_client("Parent", ciphersuite, parent_provider);
1974 let parent_group = MlsGroup::builder()
1975 .ciphersuite(ciphersuite)
1976 .build(parent_provider, &parent_signer, parent_cwk)
1977 .unwrap();
1978
1979 assert_eq!(alice_group.epoch(), 1.into());
1981 let result = alice_group
1982 .commit_builder()
1983 .branch(alice_provider.rand(), &parent_group.branch_info())
1984 .unwrap()
1985 .load_psks(alice_provider.storage())
1986 .unwrap()
1987 .build(
1988 alice_provider.rand(),
1989 alice_provider.crypto(),
1990 &alice_signer,
1991 |_| true,
1992 );
1993
1994 assert!(
1995 matches!(
1996 result,
1997 Err(CreateCommitError::ProposalValidationError(
1998 ProposalValidationError::Psk(PskError::NotAllowed)
1999 ))
2000 ),
2001 "expected a branch PSK outside the initial commit to be rejected, got {result:?}"
2002 );
2003 }
2004
2005 #[openmls_test::openmls_test]
2013 fn reinit_psk_rejected_outside_initial_commit() {
2014 let alice_provider = &Provider::default();
2015 let bob_provider = &Provider::default();
2016 let old_provider = &Provider::default();
2017
2018 let (mut alice_group, alice_signer, _bob_group, _bob_signer, _alice_cwk, _bob_cwk) =
2019 setup_alice_bob_group(ciphersuite, alice_provider, bob_provider);
2020
2021 let (old_cwk, _old_kpb, old_signer, _old_pk) =
2024 setup_client("Old", ciphersuite, old_provider);
2025 let old_group = MlsGroup::builder()
2026 .ciphersuite(ciphersuite)
2027 .build(old_provider, &old_signer, old_cwk)
2028 .unwrap();
2029 let old_secret = old_group.resumption_psk_secret().clone();
2030
2031 assert_eq!(alice_group.epoch(), 1.into());
2033 let result = alice_group
2034 .commit_builder()
2035 .reinit(
2036 alice_provider.rand(),
2037 old_group.group_id().clone(),
2038 old_group.epoch(),
2039 old_secret,
2040 )
2041 .unwrap()
2042 .load_psks(alice_provider.storage())
2043 .unwrap()
2044 .build(
2045 alice_provider.rand(),
2046 alice_provider.crypto(),
2047 &alice_signer,
2048 |_| true,
2049 );
2050
2051 assert!(
2052 matches!(
2053 result,
2054 Err(CreateCommitError::ProposalValidationError(
2055 ProposalValidationError::Psk(PskError::NotAllowed)
2056 ))
2057 ),
2058 "expected a reinit PSK outside the initial commit to be rejected, got {result:?}"
2059 );
2060 }
2061}