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, NewSignerBundle,
29 },
30 schedule::{
31 psk::{load_psks, PskSecret},
32 EpochSecretsResult, JoinerSecret, KeySchedule, PreSharedKeyId,
33 },
34 storage::{OpenMlsProvider, StorageProvider},
35 treesync::errors::LeafNodeValidationError,
36 versions::ProtocolVersion,
37};
38#[cfg(feature = "virtual-clients-draft")]
39use crate::{
40 components::vc_derivation_info::{
41 pprf_input, DerivationInfo, EmulationEpochState, EpochEncryptionKey, EpochId, EpochInfoTbe,
42 OperationSecret, VcEmulation, VcPprf, VirtualClientOperationType, VirtualClientsError,
43 VC_COMPONENT_ID,
44 },
45 extensions::{AppDataDictionary, AppDataDictionaryExtension},
46 treesync::node::leaf_node::LeafNode,
47};
48#[cfg(feature = "extensions-draft-08")]
49use crate::{
50 messages::proposals::AppDataUpdateProposal,
51 prelude::processing::{AppDataDictionaryUpdater, AppDataUpdates},
52 schedule::application_export_tree::ApplicationExportTree,
53};
54
55#[cfg(feature = "virtual-clients-draft")]
59#[derive(Debug)]
60struct VcLoaded {
61 epoch_id: EpochId,
62 emulation_leaf_index: LeafNodeIndex,
63 pprf: VcPprf,
64 epoch_encryption_key: EpochEncryptionKey,
65}
66
67pub(crate) mod external_commits;
68
69pub use external_commits::{ExternalCommitBuilder, ExternalCommitBuilderError};
70
71#[cfg(doc)]
72use super::MlsGroupJoinConfig;
73
74use super::{
75 mls_auth_content::AuthenticatedContent,
76 staged_commit::{MemberStagedCommitState, StagedCommitState},
77 AddProposal, CreateCommitResult, GroupContextExtensionProposal, MlsGroup, MlsGroupState,
78 MlsMessageOut, PendingCommitState, Proposal, RemoveProposal, Sender,
79};
80
81#[derive(Debug)]
82struct ExternalCommitInfo {
83 aad: Vec<u8>,
84 credential: CredentialWithKey,
85 wire_format_policy: WireFormatPolicy,
86}
87
88#[derive(Debug, Default)]
89struct GroupInfoConfig {
90 create_group_info: bool,
91 use_ratchet_tree_extension: bool,
92 other_extensions: Vec<Extension>,
93}
94
95#[derive(Debug)]
97pub struct Initial {
98 own_proposals: Vec<Proposal>,
99 force_self_update: bool,
100 leaf_node_parameters: LeafNodeParameters,
101 external_commit_info: Option<ExternalCommitInfo>,
102
103 consume_proposal_store: bool,
106
107 #[cfg(feature = "virtual-clients-draft")]
114 vc_emulation: Option<VcEmulation>,
115}
116
117impl Default for Initial {
118 fn default() -> Self {
119 Initial {
120 consume_proposal_store: true,
121 force_self_update: false,
122 leaf_node_parameters: LeafNodeParameters::default(),
123 own_proposals: vec![],
124 external_commit_info: None,
125 #[cfg(feature = "virtual-clients-draft")]
126 vc_emulation: None,
127 }
128 }
129}
130
131pub struct LoadedPsks {
133 own_proposals: Vec<Proposal>,
134 force_self_update: bool,
135 leaf_node_parameters: LeafNodeParameters,
136 external_commit_info: Option<ExternalCommitInfo>,
137
138 consume_proposal_store: bool,
141 psks: Vec<(PreSharedKeyId, Secret)>,
142
143 group_info_config: GroupInfoConfig,
145
146 #[cfg(feature = "extensions-draft-08")]
147 app_data_dictionary_updates: Option<AppDataUpdates>,
148
149 #[cfg(feature = "virtual-clients-draft")]
153 vc_loaded: Option<VcLoaded>,
154}
155
156#[derive(Debug)]
158pub struct Complete {
159 result: CreateCommitResult,
160 original_wire_format_policy: Option<WireFormatPolicy>,
162 #[cfg(feature = "virtual-clients-draft")]
166 vc_punctured: Option<VcPunctured>,
167}
168
169#[cfg(feature = "virtual-clients-draft")]
170#[derive(Debug)]
171struct VcPunctured {
172 epoch_id: EpochId,
173 pprf: VcPprf,
174}
175
176#[derive(Debug)]
214pub struct CommitBuilder<'a, T, G: BorrowMut<MlsGroup> = &'a mut MlsGroup> {
215 group: G,
218
219 stage: T,
221
222 pd: PhantomData<&'a ()>,
223}
224
225impl<'a, T, G: BorrowMut<MlsGroup>> CommitBuilder<'a, T, G> {
226 pub(crate) fn replace_stage<NextStage>(
227 self,
228 next_stage: NextStage,
229 ) -> (T, CommitBuilder<'a, NextStage, G>) {
230 self.map_stage(|prev_stage| (prev_stage, next_stage))
231 }
232
233 pub(crate) fn into_stage<NextStage>(
234 self,
235 next_stage: NextStage,
236 ) -> CommitBuilder<'a, NextStage, G> {
237 self.replace_stage(next_stage).1
238 }
239
240 fn take_stage(self) -> (T, CommitBuilder<'a, (), G>) {
241 self.replace_stage(())
242 }
243
244 fn map_stage<NextStage, Aux, F: FnOnce(T) -> (Aux, NextStage)>(
245 self,
246 f: F,
247 ) -> (Aux, CommitBuilder<'a, NextStage, G>) {
248 let Self {
249 group,
250 stage,
251 pd: PhantomData,
252 } = self;
253
254 let (aux, stage) = f(stage);
255
256 (
257 aux,
258 CommitBuilder {
259 group,
260 stage,
261 pd: PhantomData,
262 },
263 )
264 }
265
266 #[cfg(feature = "fork-resolution")]
267 pub(crate) fn stage(&self) -> &T {
268 &self.stage
269 }
270}
271
272impl MlsGroup {
273 pub fn commit_builder(&mut self) -> CommitBuilder<'_, Initial> {
275 CommitBuilder::<'_, Initial, &mut MlsGroup>::new(self)
276 }
277}
278
279impl<'a> CommitBuilder<'a, Initial, &mut MlsGroup> {
281 pub fn consume_proposal_store(mut self, consume_proposal_store: bool) -> Self {
284 self.stage.consume_proposal_store = consume_proposal_store;
285 self
286 }
287
288 pub fn force_self_update(mut self, force_self_update: bool) -> Self {
290 self.stage.force_self_update = force_self_update;
291 self
292 }
293
294 pub fn propose_adds(mut self, key_packages: impl IntoIterator<Item = KeyPackage>) -> Self {
297 self.stage.own_proposals.extend(
298 key_packages
299 .into_iter()
300 .map(|key_package| Proposal::add(AddProposal { key_package })),
301 );
302 self
303 }
304
305 pub fn propose_removals(mut self, removed: impl IntoIterator<Item = LeafNodeIndex>) -> Self {
308 self.stage.own_proposals.extend(
309 removed
310 .into_iter()
311 .map(|removed| Proposal::remove(RemoveProposal { removed })),
312 );
313 self
314 }
315
316 pub fn propose_group_context_extensions(
319 mut self,
320 extensions: Extensions<GroupContext>,
321 ) -> Result<Self, CreateCommitError> {
322 let proposal = GroupContextExtensionProposal::new(extensions);
323 self.stage
324 .own_proposals
325 .push(Proposal::group_context_extensions(proposal));
326 Ok(self)
327 }
328
329 pub fn add_proposal(mut self, proposal: Proposal) -> Self {
332 self.stage.own_proposals.push(proposal);
333 self
334 }
335
336 pub fn add_proposals(mut self, proposals: impl IntoIterator<Item = Proposal>) -> Self {
338 self.stage.own_proposals.extend(proposals);
339 self
340 }
341}
342
343impl<'a, G: BorrowMut<MlsGroup>> CommitBuilder<'a, Initial, G> {
345 pub fn new(group: G) -> CommitBuilder<'a, Initial, G> {
347 let stage = Initial {
348 ..Default::default()
349 };
350 CommitBuilder {
351 group,
352 stage,
353 pd: PhantomData,
354 }
355 }
356
357 pub fn leaf_node_parameters(mut self, leaf_node_parameters: LeafNodeParameters) -> Self {
360 self.stage.leaf_node_parameters = leaf_node_parameters;
361 self
362 }
363
364 #[cfg(feature = "virtual-clients-draft")]
400 pub fn vc_emulation(mut self, emulation: VcEmulation) -> Self {
401 self.stage.vc_emulation = Some(emulation);
402 self
403 }
404
405 pub fn load_psks<Storage: StorageProvider>(
407 self,
408 storage: &'a Storage,
409 ) -> Result<CommitBuilder<'a, LoadedPsks, G>, CreateCommitError> {
410 let psk_ids: Vec<_> = self
411 .stage
412 .own_proposals
413 .iter()
414 .chain(
415 self.group
416 .borrow()
417 .proposal_store()
418 .proposals()
419 .map(|queued_proposal| queued_proposal.proposal()),
420 )
421 .filter_map(|proposal| match proposal {
422 Proposal::PreSharedKey(psk_proposal) => Some(psk_proposal.clone().into_psk_id()),
423 _ => None,
424 })
425 .collect();
426
427 let psks = load_psks(storage, &self.group.borrow().resumption_psk_store, &psk_ids)?
429 .into_iter()
430 .map(|(psk_id_ref, key)| (psk_id_ref.clone(), key))
431 .collect();
432
433 let use_ratchet_tree_extension = self
435 .group
436 .borrow()
437 .configuration()
438 .use_ratchet_tree_extension;
439
440 let group_info_config = GroupInfoConfig {
441 use_ratchet_tree_extension,
442 create_group_info: use_ratchet_tree_extension,
443 other_extensions: vec![],
444 };
445
446 #[cfg(feature = "virtual-clients-draft")]
454 let vc_loaded = if let Some(emulation) = &self.stage.vc_emulation {
455 let pprf: VcPprf = storage
456 .vc_pprf(&emulation.epoch_id)
457 .map_err(|e| {
458 log::error!("vc: load pprf in load_psks failed: {e:?}");
459 CreateCommitError::VirtualClientsError(VirtualClientsError::StorageError)
460 })?
461 .ok_or(VirtualClientsError::MissingPprf)?;
462 let state: EmulationEpochState = storage
463 .vc_emulation_epoch_state(&emulation.epoch_id)
464 .map_err(|e| {
465 log::error!("vc: load emulation epoch state in load_psks failed: {e:?}");
466 CreateCommitError::VirtualClientsError(VirtualClientsError::StorageError)
467 })?
468 .ok_or(VirtualClientsError::MissingEpochEncryptionKey)?;
469 let (emulation_leaf_index, epoch_encryption_key) = state.into_parts();
470 Some(VcLoaded {
471 epoch_id: emulation.epoch_id.clone(),
472 emulation_leaf_index,
473 pprf,
474 epoch_encryption_key,
475 })
476 } else {
477 None
478 };
479
480 Ok(self
481 .map_stage(|stage| {
482 (
483 (),
484 LoadedPsks {
485 own_proposals: stage.own_proposals,
486 psks,
487 force_self_update: stage.force_self_update,
488 leaf_node_parameters: stage.leaf_node_parameters,
489 consume_proposal_store: stage.consume_proposal_store,
490 group_info_config,
491 external_commit_info: stage.external_commit_info,
492 #[cfg(feature = "extensions-draft-08")]
493 app_data_dictionary_updates: None,
494 #[cfg(feature = "virtual-clients-draft")]
495 vc_loaded,
496 },
497 )
498 })
499 .1)
500 }
501}
502
503impl<'a, G: BorrowMut<MlsGroup>> CommitBuilder<'a, LoadedPsks, G> {
504 pub fn create_group_info(mut self, create_group_info: bool) -> Self {
507 self.stage.group_info_config.create_group_info = create_group_info;
508 self
509 }
510
511 pub fn use_ratchet_tree_extension(mut self, use_ratchet_tree_extension: bool) -> Self {
514 if use_ratchet_tree_extension {
515 self.stage.group_info_config.create_group_info = true;
516 }
517 self.stage.group_info_config.use_ratchet_tree_extension = use_ratchet_tree_extension;
518 self
519 }
520
521 pub fn create_group_info_with_extensions(
526 mut self,
527 extensions: impl IntoIterator<Item = Extension>,
528 ) -> Result<Self, InvalidExtensionError> {
529 self.stage.group_info_config.create_group_info = true;
530 self.stage.group_info_config.other_extensions = extensions
531 .into_iter()
532 .map(|extension| {
533 if extension.as_ratchet_tree_extension().is_ok()
534 || extension.as_external_pub_extension().is_ok()
535 {
536 Err(InvalidExtensionError::CannotAddDirectlyToGroupInfo)
537 } else {
538 Ok(extension)
539 }
540 })
541 .collect::<Result<Vec<_>, _>>()?;
542
543 Ok(self)
544 }
545 pub fn build<S: Signer>(
549 self,
550 rand: &impl OpenMlsRand,
551 crypto: &impl OpenMlsCrypto,
552 signer: &S,
553 f: impl FnMut(&QueuedProposal) -> bool,
554 ) -> Result<CommitBuilder<'a, Complete, G>, CreateCommitError> {
555 self.build_internal(rand, crypto, signer, None::<NewSignerBundle<'_, S>>, f)
556 }
557
558 pub fn build_with_new_signer<S: Signer>(
566 self,
567 rand: &impl OpenMlsRand,
568 crypto: &impl OpenMlsCrypto,
569 old_signer: &impl Signer,
570 new_signer: NewSignerBundle<'_, S>,
571 f: impl FnMut(&QueuedProposal) -> bool,
572 ) -> Result<CommitBuilder<'a, Complete, G>, CreateCommitError> {
573 self.build_internal(rand, crypto, old_signer, Some(new_signer), f)
574 }
575
576 fn build_internal<S: Signer>(
577 self,
578 rand: &impl OpenMlsRand,
579 crypto: &impl OpenMlsCrypto,
580 old_signer: &impl Signer,
581 new_signer: Option<NewSignerBundle<'_, S>>,
582 f: impl FnMut(&QueuedProposal) -> bool,
583 ) -> Result<CommitBuilder<'a, Complete, G>, CreateCommitError> {
584 let (mut cur_stage, builder) = self.take_stage();
585
586 let GroupInfoConfig {
588 create_group_info,
589 use_ratchet_tree_extension,
590 other_extensions,
591 } = cur_stage.group_info_config;
592
593 let group = builder.group.borrow();
594 let ciphersuite = group.ciphersuite();
595 let own_leaf_index = group.own_leaf_index();
596 let (sender, is_external_commit) = match cur_stage.external_commit_info {
597 None => (Sender::build_member(own_leaf_index), false),
598 Some(_) => (Sender::NewMemberCommit, true),
599 };
600 let psks = cur_stage.psks;
601
602 let own_proposals: Vec<_> = cur_stage
605 .own_proposals
606 .into_iter()
607 .map(|proposal| {
608 QueuedProposal::from_proposal_and_sender(ciphersuite, crypto, proposal, &sender)
609 })
610 .collect::<Result<_, _>>()?;
611
612 let group_proposal_store_queue = group
615 .pending_proposals()
616 .filter(|_| cur_stage.consume_proposal_store)
617 .cloned();
618
619 let proposal_queue = group_proposal_store_queue.chain(own_proposals).filter(f);
623
624 let (proposal_queue, contains_own_updates) =
625 ProposalQueue::filter_proposals(proposal_queue, group.own_leaf_index).map_err(|e| {
626 match e {
627 ProposalQueueError::LibraryError(e) => e.into(),
628 ProposalQueueError::ProposalNotFound => CreateCommitError::MissingProposal,
629 ProposalQueueError::UpdateFromExternalSender
630 | ProposalQueueError::SelfRemoveFromNonMember => {
631 CreateCommitError::WrongProposalSenderType
632 }
633 }
634 })?;
635
636 group
641 .public_group
642 .validate_proposal_type_support(&proposal_queue)?;
643 group
648 .public_group
649 .validate_key_uniqueness(&proposal_queue, None)?;
650 group.public_group.validate_add_proposals(&proposal_queue)?;
652 group.public_group.validate_capabilities(&proposal_queue)?;
655 group
658 .public_group
659 .validate_remove_proposals(&proposal_queue)?;
660 group
661 .public_group
662 .validate_pre_shared_key_proposals(&proposal_queue)?;
663 group
668 .public_group
669 .validate_update_proposals(&proposal_queue, own_leaf_index)?;
670
671 group
674 .public_group
675 .validate_group_context_extensions_proposal(&proposal_queue)?;
676
677 #[cfg(feature = "extensions-draft-08")]
678 group
679 .public_group
680 .validate_app_data_update_proposals_and_group_context(&proposal_queue)?;
681
682 if is_external_commit {
683 group
684 .public_group
685 .validate_external_commit(&proposal_queue)?;
686 }
687
688 let proposal_reference_list = proposal_queue.commit_list();
689
690 let mut diff = group.public_group.empty_diff();
692
693 #[cfg(feature = "extensions-draft-08")]
695 let apply_proposals_values = diff.apply_proposals_with_app_data_updates(
696 &proposal_queue,
697 own_leaf_index,
698 cur_stage.app_data_dictionary_updates,
699 )?;
700 #[cfg(not(feature = "extensions-draft-08"))]
701 let apply_proposals_values = diff.apply_proposals(&proposal_queue, own_leaf_index)?;
702 if apply_proposals_values.self_removed && !is_external_commit {
703 return Err(CreateCommitError::CannotRemoveSelf);
704 }
705
706 #[cfg(feature = "virtual-clients-draft")]
715 let own_update_override = if let Some(loaded) = cur_stage.vc_loaded.as_mut() {
716 let resolved_dictionary = check_vc_leaf_configuration(
721 &cur_stage.leaf_node_parameters,
722 group,
723 own_leaf_index,
724 is_external_commit,
725 )?;
726
727 Some(apply_vc_emulation(
728 loaded,
729 &mut cur_stage.leaf_node_parameters,
730 resolved_dictionary,
731 rand,
732 crypto,
733 ciphersuite,
734 )?)
735 } else {
736 None
737 };
738 #[cfg(not(feature = "virtual-clients-draft"))]
739 let own_update_override: Option<crate::treesync::diff::OwnUpdatePathOverride> = None;
740
741 let path_computation_result =
742 if apply_proposals_values.path_required
744 || contains_own_updates
745 || cur_stage.force_self_update
746 || !cur_stage.leaf_node_parameters.is_empty()
747 {
748 let commit_type = match &cur_stage.external_commit_info {
749 Some(ExternalCommitInfo { credential , ..}) => {
750 CommitType::External(credential.clone())
751 }
752 None => CommitType::Member,
753 };
754 if let Some(new_signer) = new_signer {
758 if let Some(credential_with_key) =
759 cur_stage.leaf_node_parameters.credential_with_key()
760 {
761 if credential_with_key != &new_signer.credential_with_key {
762 return Err(CreateCommitError::InvalidLeafNodeParameters);
763 }
764 }
765 cur_stage.leaf_node_parameters.set_credential_with_key(
766 new_signer.credential_with_key,
767 );
768
769 diff.compute_path(
770 rand,
771 crypto,
772 own_leaf_index,
773 apply_proposals_values.exclusion_list(),
774 &commit_type,
775 &cur_stage.leaf_node_parameters,
776 new_signer.signer,
777 apply_proposals_values.extensions.clone(),
778 own_update_override,
779 )?
780 } else {
781 diff.compute_path(
782 rand,
783 crypto,
784 own_leaf_index,
785 apply_proposals_values.exclusion_list(),
786 &commit_type,
787 &cur_stage.leaf_node_parameters,
788 old_signer,
789 apply_proposals_values.extensions.clone(),
790 own_update_override,
791 )?
792 }
793 } else {
794 diff.update_group_context(crypto, apply_proposals_values.extensions.clone())?;
797 PathComputationResult::default()
798 };
799
800 let update_path_leaf_node = path_computation_result
801 .encrypted_path
802 .as_ref()
803 .map(|path| path.leaf_node().clone());
804
805 if let Some(ref leaf_node) = update_path_leaf_node {
807 if !diff
813 .group_context()
814 .extensions()
815 .iter()
816 .map(Extension::extension_type)
817 .all(|ext_type| leaf_node.supports_extension(&ext_type))
818 {
819 return Err(CreateCommitError::LeafNodeValidation(
820 LeafNodeValidationError::UnsupportedExtensions,
821 ));
822 }
823
824 if let Some(required_capabilities) =
827 diff.group_context().extensions().required_capabilities()
828 {
829 leaf_node
830 .capabilities()
831 .supports_required_capabilities(required_capabilities)?
832 }
833 }
834
835 let commit = Commit {
837 proposals: proposal_reference_list,
838 path: path_computation_result.encrypted_path,
839 };
840
841 let (outgoing_aad, wire_format): (Vec<u8>, WireFormat) =
842 match &cur_stage.external_commit_info {
843 None => (
844 group.outgoing_authenticated_data()?,
845 group.outgoing_wire_format(),
846 ),
847 Some(ExternalCommitInfo { aad, .. }) => {
848 #[cfg(feature = "extensions-draft-08")]
852 let aad_bytes = if group.context().safe_aad_required() {
853 crate::framing::safe_aad::assemble_authenticated_data(
854 &crate::framing::SafeAad::empty(),
855 aad,
856 )
857 .map_err(|_| LibraryError::custom("SafeAad serialization failed"))?
858 } else {
859 aad.clone()
860 };
861 #[cfg(not(feature = "extensions-draft-08"))]
862 let aad_bytes = aad.clone();
863 (aad_bytes, WireFormat::PublicMessage)
864 }
865 };
866 let framing_parameters = FramingParameters::new(&outgoing_aad, wire_format);
867
868 let mut authenticated_content = AuthenticatedContent::commit(
870 framing_parameters,
871 sender,
872 commit,
873 group.public_group.group_context(),
874 old_signer,
875 )?;
876
877 diff.update_confirmed_transcript_hash(crypto, &authenticated_content)?;
879
880 let serialized_provisional_group_context = diff
881 .group_context()
882 .tls_serialize_detached()
883 .map_err(LibraryError::missing_bound_check)?;
884
885 let joiner_secret = JoinerSecret::new(
886 crypto,
887 ciphersuite,
888 path_computation_result.commit_secret,
889 group.group_epoch_secrets().init_secret(),
890 &serialized_provisional_group_context,
891 )
892 .map_err(LibraryError::unexpected_crypto_error)?;
893
894 let psk_secret = { PskSecret::new(crypto, ciphersuite, psks)? };
896
897 let mut key_schedule = KeySchedule::init(ciphersuite, crypto, &joiner_secret, psk_secret)?;
899
900 let serialized_provisional_group_context = diff
901 .group_context()
902 .tls_serialize_detached()
903 .map_err(LibraryError::missing_bound_check)?;
904
905 let welcome_secret = key_schedule
906 .welcome(crypto, ciphersuite)
907 .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
908 key_schedule
909 .add_context(crypto, &serialized_provisional_group_context)
910 .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
911 let EpochSecretsResult {
912 epoch_secrets: provisional_epoch_secrets,
913 #[cfg(feature = "extensions-draft-08")]
914 application_exporter,
915 } = key_schedule
916 .epoch_secrets(crypto, ciphersuite)
917 .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
918
919 let confirmation_tag = provisional_epoch_secrets
921 .confirmation_key()
922 .tag(
923 crypto,
924 ciphersuite,
925 diff.group_context().confirmed_transcript_hash(),
926 )
927 .map_err(LibraryError::unexpected_crypto_error)?;
928
929 authenticated_content.set_confirmation_tag(confirmation_tag.clone());
931
932 diff.update_interim_transcript_hash(ciphersuite, crypto, confirmation_tag.clone())?;
933
934 let needs_welcome = !apply_proposals_values.invitation_list.is_empty();
936
937 let needs_group_info = needs_welcome || create_group_info;
941
942 let (welcome_option, group_info) = if !needs_group_info {
943 (None, None)
944 } else {
945 let mut extensions_list = vec![];
947 if use_ratchet_tree_extension {
948 extensions_list.push(Extension::RatchetTree(RatchetTreeExtension::new(
949 diff.export_ratchet_tree(),
950 )));
951 };
952 extensions_list.extend(other_extensions);
954
955 let mut extensions = Extensions::from_vec(extensions_list)?;
956
957 let welcome_option = needs_welcome
958 .then(|| -> Result<_, CreateCommitError> {
959 let group_info_tbs = {
960 GroupInfoTBS::new(
961 diff.group_context().clone(),
962 extensions.clone(),
963 confirmation_tag.clone(),
964 own_leaf_index,
965 )?
966 };
967 let group_info = group_info_tbs.sign(old_signer)?;
969
970 let (welcome_key, welcome_nonce) = welcome_secret
972 .derive_welcome_key_nonce(crypto, ciphersuite)
973 .map_err(LibraryError::unexpected_crypto_error)?;
974 let encrypted_group_info = welcome_key
975 .aead_seal(
976 crypto,
977 group_info
978 .tls_serialize_detached()
979 .map_err(LibraryError::missing_bound_check)?
980 .as_slice(),
981 &[],
982 &welcome_nonce,
983 )
984 .map_err(LibraryError::unexpected_crypto_error)?;
985
986 let encrypted_secrets = diff.encrypt_group_secrets(
989 &joiner_secret,
990 apply_proposals_values.invitation_list,
991 path_computation_result.plain_path.as_deref(),
992 &apply_proposals_values.presharedkeys,
993 &encrypted_group_info,
994 crypto,
995 own_leaf_index,
996 )?;
997
998 let welcome =
1000 Welcome::new(ciphersuite, encrypted_secrets, encrypted_group_info);
1001 Ok(welcome)
1002 })
1003 .transpose()?;
1004
1005 let exported_group_info = create_group_info
1008 .then(|| -> Result<_, CreateCommitError> {
1009 let external_pub = provisional_epoch_secrets
1010 .external_secret()
1011 .derive_external_keypair(crypto, ciphersuite)
1012 .map_err(LibraryError::unexpected_crypto_error)?
1013 .public;
1014
1015 let external_pub_extension =
1016 Extension::ExternalPub(ExternalPubExtension::new(external_pub.into()));
1017 extensions.add(external_pub_extension)?;
1018 let group_info_tbs = {
1019 GroupInfoTBS::new(
1020 diff.group_context().clone(),
1021 extensions,
1022 confirmation_tag.clone(),
1023 own_leaf_index,
1024 )?
1025 };
1026 Ok(group_info_tbs.sign(old_signer)?)
1028 })
1029 .transpose()?;
1030
1031 (welcome_option, exported_group_info)
1032 };
1033
1034 let (provisional_group_epoch_secrets, provisional_message_secrets) =
1035 provisional_epoch_secrets.split_secrets(
1036 serialized_provisional_group_context,
1037 diff.tree_size(),
1038 own_leaf_index,
1039 );
1040
1041 #[cfg(feature = "extensions-draft-08")]
1042 let application_export_tree = ApplicationExportTree::new(application_exporter);
1043 let staged_commit_state = MemberStagedCommitState::new(
1044 provisional_group_epoch_secrets,
1045 provisional_message_secrets,
1046 diff.into_staged_diff(crypto, ciphersuite)?,
1047 path_computation_result.new_keypairs,
1048 None,
1051 update_path_leaf_node,
1052 #[cfg(feature = "extensions-draft-08")]
1053 application_export_tree,
1054 );
1055 let staged_commit = StagedCommit::new(
1056 proposal_queue,
1057 StagedCommitState::GroupMember(Box::new(staged_commit_state)),
1058 );
1059
1060 Ok(builder.into_stage(Complete {
1061 result: CreateCommitResult {
1062 commit: authenticated_content,
1063 welcome_option,
1064 staged_commit,
1065 group_info: group_info.filter(|_| create_group_info),
1066 },
1067 original_wire_format_policy: cur_stage
1068 .external_commit_info
1069 .as_ref()
1070 .map(|info| info.wire_format_policy),
1071 #[cfg(feature = "virtual-clients-draft")]
1072 vc_punctured: cur_stage.vc_loaded.map(|loaded| VcPunctured {
1073 epoch_id: loaded.epoch_id,
1074 pprf: loaded.pprf,
1075 }),
1076 }))
1077 }
1078
1079 #[cfg(feature = "extensions-draft-08")]
1084 pub fn app_data_dictionary_updater(&self) -> AppDataDictionaryUpdater<'_> {
1085 AppDataDictionaryUpdater::new(self.group.borrow().context().app_data_dict())
1086 }
1087
1088 #[cfg(feature = "extensions-draft-08")]
1090 pub fn with_app_data_dictionary_updates(
1091 &mut self,
1092 app_data_dictionary_updates: Option<AppDataUpdates>,
1093 ) {
1094 self.stage.app_data_dictionary_updates = app_data_dictionary_updates;
1095 }
1096
1097 #[cfg(feature = "extensions-draft-08")]
1099 pub fn app_data_update_proposals(&self) -> impl Iterator<Item = &AppDataUpdateProposal> {
1100 let proposal_store_proposals = self
1101 .group
1102 .borrow()
1103 .proposal_store()
1104 .proposals()
1105 .map(|queued_proposal| queued_proposal.proposal());
1106
1107 let all_proposals = proposal_store_proposals.chain(self.stage.own_proposals.iter());
1109
1110 let mut app_data_update_proposals: Vec<&AppDataUpdateProposal> = all_proposals
1112 .filter_map(|proposal| match proposal {
1113 Proposal::AppDataUpdate(proposal) => Some(proposal.as_ref()),
1114 _ => None,
1115 })
1116 .collect();
1117
1118 app_data_update_proposals.sort_by_key(|prop| prop.component_id());
1119 app_data_update_proposals.into_iter()
1120 }
1121}
1122
1123impl CommitBuilder<'_, Complete, &mut MlsGroup> {
1125 #[cfg(test)]
1126 pub(crate) fn commit_result(self) -> CreateCommitResult {
1127 self.stage.result
1128 }
1129
1130 pub fn stage_commit<Provider: OpenMlsProvider>(
1132 self,
1133 provider: &Provider,
1134 ) -> Result<CommitMessageBundle, CommitBuilderStageError<Provider::StorageError>> {
1135 let Self {
1136 group,
1137 stage:
1138 Complete {
1139 result: create_commit_result,
1140 original_wire_format_policy: _,
1141 #[cfg(feature = "virtual-clients-draft")]
1142 vc_punctured,
1143 },
1144 ..
1145 } = self;
1146
1147 group.group_state = MlsGroupState::PendingCommit(Box::new(PendingCommitState::Member(
1150 create_commit_result.staged_commit,
1151 )));
1152
1153 provider
1154 .storage()
1155 .write_group_state(group.group_id(), &group.group_state)
1156 .map_err(CommitBuilderStageError::KeyStoreError)?;
1157
1158 #[cfg(feature = "virtual-clients-draft")]
1160 if let Some(VcPunctured { epoch_id, pprf }) = vc_punctured {
1161 provider
1162 .storage()
1163 .write_vc_pprf(&epoch_id, &pprf)
1164 .map_err(CommitBuilderStageError::KeyStoreError)?;
1165 }
1166
1167 group.reset_aad();
1168
1169 let mls_message = group.content_to_mls_message(create_commit_result.commit, provider)?;
1175
1176 Ok(CommitMessageBundle {
1177 version: group.version(),
1178 commit: mls_message,
1179 welcome: create_commit_result.welcome_option,
1180 group_info: create_commit_result.group_info,
1181 })
1182 }
1183}
1184
1185#[cfg(feature = "virtual-clients-draft")]
1189fn apply_vc_emulation(
1190 loaded: &mut VcLoaded,
1191 leaf_node_parameters: &mut LeafNodeParameters,
1192 resolved_dictionary: AppDataDictionary,
1193 rand: &impl OpenMlsRand,
1194 crypto: &impl OpenMlsCrypto,
1195 ciphersuite: openmls_traits::types::Ciphersuite,
1196) -> Result<crate::treesync::diff::OwnUpdatePathOverride, CreateCommitError> {
1197 const VC_RANDOMNESS_SIZE: usize = 32;
1202 let random = rand.random_vec(VC_RANDOMNESS_SIZE).map_err(|e| {
1203 log::error!("vc: per-commit randomness failed: {e:?}");
1204 VirtualClientsError::RandError
1205 })?;
1206 let epoch_info = EpochInfoTbe {
1207 operation_type: VirtualClientOperationType::LeafNode,
1211 leaf_index: loaded.emulation_leaf_index,
1212 random,
1213 };
1214
1215 let pprf_input_bytes = pprf_input(crypto, ciphersuite, &epoch_info)?;
1216 let operation_secret: OperationSecret = loaded
1217 .pprf
1218 .evaluate(crypto, ciphersuite, &pprf_input_bytes)
1219 .map_err(VirtualClientsError::from)?
1220 .into();
1221
1222 let encrypted_epoch_info = epoch_info.encrypt(
1223 crypto,
1224 rand,
1225 ciphersuite,
1226 &loaded.epoch_encryption_key,
1227 &loaded.epoch_id,
1228 )?;
1229 let derivation_info = DerivationInfo::new(loaded.epoch_id.clone(), encrypted_epoch_info);
1230 let derivation_info_bytes = derivation_info
1231 .tls_serialize_detached()
1232 .map_err(VirtualClientsError::from)?;
1233
1234 inject_vc_derivation_info(
1235 leaf_node_parameters,
1236 resolved_dictionary,
1237 derivation_info_bytes,
1238 )?;
1239
1240 let path_secret = operation_secret
1241 .derive_path_generation_secret(crypto, ciphersuite)?
1242 .into();
1243 let leaf_encryption_keypair = operation_secret
1244 .derive_encryption_key_secret(crypto, ciphersuite)?
1245 .generate_encryption_key_pair(crypto, ciphersuite)?;
1246 Ok(crate::treesync::diff::OwnUpdatePathOverride {
1247 path_secret,
1248 leaf_encryption_keypair,
1249 })
1250}
1251
1252#[cfg(feature = "virtual-clients-draft")]
1264fn check_vc_leaf_configuration(
1265 leaf_node_parameters: &LeafNodeParameters,
1266 group: &MlsGroup,
1267 own_leaf_index: LeafNodeIndex,
1268 is_external_commit: bool,
1269) -> Result<AppDataDictionary, CreateCommitError> {
1270 use crate::{
1271 component::{ComponentId, ComponentType},
1272 extensions::ExtensionType,
1273 };
1274 use tls_codec::DeserializeBytes as _;
1275
1276 let current_leaf = if is_external_commit {
1277 None
1278 } else {
1279 Some(group.public_group().leaf(own_leaf_index).ok_or_else(|| {
1280 LibraryError::custom("Couldn't find own leaf for VC capability check")
1281 })?)
1282 };
1283
1284 let supports_app_data_dictionary = match leaf_node_parameters.capabilities() {
1285 Some(c) => c.extensions().contains(&ExtensionType::AppDataDictionary),
1286 None => current_leaf
1287 .map(|leaf| {
1288 leaf.capabilities()
1289 .extensions()
1290 .contains(&ExtensionType::AppDataDictionary)
1291 })
1292 .unwrap_or(false),
1293 };
1294 if !supports_app_data_dictionary {
1295 return Err(CreateCommitError::VirtualClientsError(
1296 VirtualClientsError::AppDataDictionaryNotSupported,
1297 ));
1298 }
1299
1300 let mut resolved_dictionary = current_leaf
1304 .and_then(|leaf| leaf.extensions().app_data_dictionary())
1305 .map(|ext| ext.dictionary().clone())
1306 .unwrap_or_default();
1307 if let Some(caller_dict) = leaf_node_parameters
1308 .extensions()
1309 .and_then(|exts| exts.app_data_dictionary())
1310 {
1311 for entry in caller_dict.dictionary().entries() {
1312 resolved_dictionary.insert(entry.id(), entry.data().to_vec());
1313 }
1314 }
1315
1316 let app_components_bytes = resolved_dictionary
1317 .get(&ComponentId::from(ComponentType::AppComponents))
1318 .map(|bytes| bytes.to_vec());
1319 let Some(app_components_bytes) = app_components_bytes else {
1320 return Err(CreateCommitError::VirtualClientsError(
1321 VirtualClientsError::VcComponentNotListed,
1322 ));
1323 };
1324
1325 let supported_components = Vec::<u16>::tls_deserialize_exact_bytes(&app_components_bytes)
1329 .map_err(|e| {
1330 log::error!("vc: AppComponents body failed to deserialize: {e:?}");
1331 CreateCommitError::VirtualClientsError(VirtualClientsError::VcComponentNotListed)
1332 })?;
1333 if !supported_components.contains(&VC_COMPONENT_ID) {
1334 return Err(CreateCommitError::VirtualClientsError(
1335 VirtualClientsError::VcComponentNotListed,
1336 ));
1337 }
1338
1339 Ok(resolved_dictionary)
1340}
1341
1342#[cfg(feature = "virtual-clients-draft")]
1347fn inject_vc_derivation_info(
1348 leaf_node_parameters: &mut LeafNodeParameters,
1349 mut resolved_dictionary: AppDataDictionary,
1350 derivation_info_bytes: Vec<u8>,
1351) -> Result<(), CreateCommitError> {
1352 resolved_dictionary.insert(VC_COMPONENT_ID, derivation_info_bytes);
1353 let vc_extension =
1354 Extension::AppDataDictionary(AppDataDictionaryExtension::new(resolved_dictionary));
1355
1356 let other_extensions = leaf_node_parameters
1360 .extensions()
1361 .map(|exts| {
1362 exts.iter()
1363 .filter(|ext| !matches!(ext, Extension::AppDataDictionary(_)))
1364 .cloned()
1365 .collect::<Vec<_>>()
1366 })
1367 .unwrap_or_default();
1368 let new_extensions: Vec<Extension> = other_extensions
1369 .into_iter()
1370 .chain(std::iter::once(vc_extension))
1371 .collect();
1372 let extensions = crate::extensions::Extensions::<LeafNode>::from_vec(new_extensions)
1373 .map_err(|_| LibraryError::custom("Failed to build leaf-node extensions"))?;
1374
1375 leaf_node_parameters.set_extensions(extensions);
1376 Ok(())
1377}
1378
1379#[derive(Debug, Clone)]
1382pub struct CommitMessageBundle {
1383 version: ProtocolVersion,
1384 commit: MlsMessageOut,
1385 welcome: Option<Welcome>,
1386 group_info: Option<GroupInfo>,
1387}
1388
1389pub struct WelcomeCommitMessages {
1394 pub commit: MlsMessageOut,
1396
1397 pub welcome: MlsMessageOut,
1399
1400 pub group_info: Option<MlsMessageOut>,
1402}
1403
1404impl TryFrom<CommitMessageBundle> for WelcomeCommitMessages {
1405 type Error = LibraryError;
1406
1407 fn try_from(value: CommitMessageBundle) -> Result<Self, Self::Error> {
1408 let (commit, welcome_opt, group_info) = value.into_messages();
1409 Ok(Self {
1410 commit,
1411 welcome: welcome_opt.ok_or(LibraryError::custom(
1412 "WelcomeCommitMessages must only be used with commits that produce a welcome.",
1413 ))?,
1414 group_info,
1415 })
1416 }
1417}
1418
1419#[cfg(test)]
1420impl CommitMessageBundle {
1421 pub fn new(
1422 version: ProtocolVersion,
1423 commit: MlsMessageOut,
1424 welcome: Option<Welcome>,
1425 group_info: Option<GroupInfo>,
1426 ) -> Self {
1427 Self {
1428 version,
1429 commit,
1430 welcome,
1431 group_info,
1432 }
1433 }
1434}
1435
1436impl CommitMessageBundle {
1437 pub fn commit(&self) -> &MlsMessageOut {
1441 &self.commit
1442 }
1443
1444 pub fn welcome(&self) -> Option<&Welcome> {
1447 self.welcome.as_ref()
1448 }
1449
1450 pub fn to_welcome_msg(&self) -> Option<MlsMessageOut> {
1453 self.welcome
1454 .as_ref()
1455 .map(|welcome| MlsMessageOut::from_welcome(welcome.clone(), self.version))
1456 }
1457
1458 pub fn group_info(&self) -> Option<&GroupInfo> {
1462 self.group_info.as_ref()
1463 }
1464
1465 pub fn contents(&self) -> (&MlsMessageOut, Option<&Welcome>, Option<&GroupInfo>) {
1468 (
1469 &self.commit,
1470 self.welcome.as_ref(),
1471 self.group_info.as_ref(),
1472 )
1473 }
1474
1475 pub fn into_commit(self) -> MlsMessageOut {
1479 self.commit
1480 }
1481
1482 pub fn into_welcome(self) -> Option<Welcome> {
1486 self.welcome
1487 }
1488
1489 pub fn into_welcome_msg(self) -> Option<MlsMessageOut> {
1492 self.welcome
1493 .map(|welcome| MlsMessageOut::from_welcome(welcome, self.version))
1494 }
1495
1496 pub fn into_group_info(self) -> Option<GroupInfo> {
1501 self.group_info
1502 }
1503
1504 pub fn into_group_info_msg(self) -> Option<MlsMessageOut> {
1506 self.group_info.map(|group_info| group_info.into())
1507 }
1508
1509 pub fn into_contents(self) -> (MlsMessageOut, Option<Welcome>, Option<GroupInfo>) {
1512 (self.commit, self.welcome, self.group_info)
1513 }
1514
1515 pub fn into_messages(self) -> (MlsMessageOut, Option<MlsMessageOut>, Option<MlsMessageOut>) {
1518 (
1519 self.commit,
1520 self.welcome
1521 .map(|welcome| MlsMessageOut::from_welcome(welcome, self.version)),
1522 self.group_info.map(|group_info| group_info.into()),
1523 )
1524 }
1525}
1526
1527impl IntoIterator for CommitMessageBundle {
1528 type Item = MlsMessageOut;
1529
1530 type IntoIter = core::iter::Chain<
1531 core::iter::Chain<
1532 core::option::IntoIter<MlsMessageOut>,
1533 core::option::IntoIter<MlsMessageOut>,
1534 >,
1535 core::option::IntoIter<MlsMessageOut>,
1536 >;
1537
1538 fn into_iter(self) -> Self::IntoIter {
1539 let welcome = self.to_welcome_msg();
1540 let group_info = self.group_info.map(|group_info| group_info.into());
1541
1542 Some(self.commit)
1543 .into_iter()
1544 .chain(welcome)
1545 .chain(group_info)
1546 }
1547}