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 DerivationInfo, DerivationInfoTbe, EmulationEpochState, EpochEncryptionKey, EpochId,
42 ExternalInitSecret, OperationSecret, VirtualClientOperationType, VirtualClientsError,
43 },
44 components::vc_operation_tree::OperationSecretTree,
45 extensions::AppDataDictionary,
46};
47#[cfg(feature = "extensions-draft")]
48use crate::{
49 messages::proposals::AppDataUpdateProposal,
50 prelude::processing::{AppDataDictionaryUpdater, AppDataUpdates},
51 schedule::application_export_tree::ApplicationExportTree,
52};
53
54#[cfg(feature = "virtual-clients-draft")]
64#[derive(Debug)]
65struct VcLoaded {
66 epoch_id: EpochId,
67 emulation_leaf_index: LeafNodeIndex,
68 epoch_encryption_key: EpochEncryptionKey,
69 emulation_ciphersuite: openmls_traits::types::Ciphersuite,
70 generation: u32,
71 operation_secret: OperationSecret,
72 resolved_dictionary: AppDataDictionary,
76}
77
78pub(crate) mod external_commits;
79
80pub use external_commits::{ExternalCommitBuilder, ExternalCommitBuilderError};
81
82#[cfg(doc)]
83use super::MlsGroupJoinConfig;
84
85use super::{
86 mls_auth_content::AuthenticatedContent,
87 staged_commit::{MemberStagedCommitState, StagedCommitState},
88 AddProposal, CreateCommitResult, GroupContextExtensionProposal, MlsGroup, MlsGroupState,
89 MlsMessageOut, PendingCommitState, Proposal, RemoveProposal, Sender,
90};
91
92#[cfg(feature = "virtual-clients-draft")]
93use super::HandshakeConfirmationData;
94
95#[derive(Debug)]
96struct ExternalCommitInfo {
97 aad: Vec<u8>,
98 credential: CredentialWithKey,
99 wire_format_policy: WireFormatPolicy,
100}
101
102#[derive(Debug, Default)]
103struct GroupInfoConfig {
104 create_group_info: bool,
105 use_ratchet_tree_extension: bool,
106 other_extensions: Vec<Extension>,
107}
108
109#[derive(Debug)]
111pub struct Initial {
112 own_proposals: Vec<Proposal>,
113 force_self_update: bool,
114 leaf_node_parameters: LeafNodeParameters,
115 external_commit_info: Option<ExternalCommitInfo>,
116
117 consume_proposal_store: bool,
120}
121
122impl Default for Initial {
123 fn default() -> Self {
124 Initial {
125 consume_proposal_store: true,
126 force_self_update: false,
127 leaf_node_parameters: LeafNodeParameters::default(),
128 own_proposals: vec![],
129 external_commit_info: None,
130 }
131 }
132}
133
134pub struct LoadedPsks {
136 own_proposals: Vec<Proposal>,
137 force_self_update: bool,
138 leaf_node_parameters: LeafNodeParameters,
139 external_commit_info: Option<ExternalCommitInfo>,
140
141 consume_proposal_store: bool,
144 psks: Vec<(PreSharedKeyId, Secret)>,
145
146 group_info_config: GroupInfoConfig,
148
149 #[cfg(feature = "extensions-draft")]
150 app_data_dictionary_updates: Option<AppDataUpdates>,
151}
152
153#[derive(Debug)]
155pub struct Complete {
156 result: CreateCommitResult,
157 original_wire_format_policy: Option<WireFormatPolicy>,
159}
160
161#[derive(Debug)]
199pub struct CommitBuilder<'a, T, G: BorrowMut<MlsGroup> = &'a mut MlsGroup> {
200 group: G,
203
204 stage: T,
206
207 #[cfg(feature = "virtual-clients-draft")]
211 vc_loaded: Option<VcLoaded>,
212
213 pd: PhantomData<&'a ()>,
214}
215
216impl<'a, T, G: BorrowMut<MlsGroup>> CommitBuilder<'a, T, G> {
217 pub(crate) fn replace_stage<NextStage>(
218 self,
219 next_stage: NextStage,
220 ) -> (T, CommitBuilder<'a, NextStage, G>) {
221 self.map_stage(|prev_stage| (prev_stage, next_stage))
222 }
223
224 pub(crate) fn into_stage<NextStage>(
225 self,
226 next_stage: NextStage,
227 ) -> CommitBuilder<'a, NextStage, G> {
228 self.replace_stage(next_stage).1
229 }
230
231 fn take_stage(self) -> (T, CommitBuilder<'a, (), G>) {
232 self.replace_stage(())
233 }
234
235 fn map_stage<NextStage, Aux, F: FnOnce(T) -> (Aux, NextStage)>(
236 self,
237 f: F,
238 ) -> (Aux, CommitBuilder<'a, NextStage, G>) {
239 let Self {
240 group,
241 stage,
242 #[cfg(feature = "virtual-clients-draft")]
243 vc_loaded,
244 pd: PhantomData,
245 } = self;
246
247 let (aux, stage) = f(stage);
248
249 (
250 aux,
251 CommitBuilder {
252 group,
253 stage,
254 #[cfg(feature = "virtual-clients-draft")]
255 vc_loaded,
256 pd: PhantomData,
257 },
258 )
259 }
260
261 #[cfg(feature = "fork-resolution")]
262 pub(crate) fn stage(&self) -> &T {
263 &self.stage
264 }
265}
266
267impl MlsGroup {
268 pub fn commit_builder(&mut self) -> CommitBuilder<'_, Initial> {
270 CommitBuilder::<'_, Initial, &mut MlsGroup>::new(self)
271 }
272}
273
274impl<'a> CommitBuilder<'a, Initial, &mut MlsGroup> {
276 pub fn consume_proposal_store(mut self, consume_proposal_store: bool) -> Self {
279 self.stage.consume_proposal_store = consume_proposal_store;
280 self
281 }
282
283 pub fn force_self_update(mut self, force_self_update: bool) -> Self {
285 self.stage.force_self_update = force_self_update;
286 self
287 }
288
289 pub fn propose_adds(mut self, key_packages: impl IntoIterator<Item = KeyPackage>) -> Self {
292 self.stage.own_proposals.extend(
293 key_packages
294 .into_iter()
295 .map(|key_package| Proposal::add(AddProposal { key_package })),
296 );
297 self
298 }
299
300 pub fn propose_removals(mut self, removed: impl IntoIterator<Item = LeafNodeIndex>) -> Self {
303 self.stage.own_proposals.extend(
304 removed
305 .into_iter()
306 .map(|removed| Proposal::remove(RemoveProposal { removed })),
307 );
308 self
309 }
310
311 pub fn propose_group_context_extensions(
314 mut self,
315 extensions: Extensions<GroupContext>,
316 ) -> Result<Self, CreateCommitError> {
317 let proposal = GroupContextExtensionProposal::new(extensions);
318 self.stage
319 .own_proposals
320 .push(Proposal::group_context_extensions(proposal));
321 Ok(self)
322 }
323
324 pub fn add_proposal(mut self, proposal: Proposal) -> Self {
327 self.stage.own_proposals.push(proposal);
328 self
329 }
330
331 pub fn add_proposals(mut self, proposals: impl IntoIterator<Item = Proposal>) -> Self {
333 self.stage.own_proposals.extend(proposals);
334 self
335 }
336}
337
338impl<'a, G: BorrowMut<MlsGroup>> CommitBuilder<'a, Initial, G> {
340 pub fn new(group: G) -> CommitBuilder<'a, Initial, G> {
342 let stage = Initial {
343 ..Default::default()
344 };
345 CommitBuilder {
346 group,
347 stage,
348 #[cfg(feature = "virtual-clients-draft")]
349 vc_loaded: None,
350 pd: PhantomData,
351 }
352 }
353
354 pub fn leaf_node_parameters(mut self, leaf_node_parameters: LeafNodeParameters) -> Self {
357 self.stage.leaf_node_parameters = leaf_node_parameters;
358 self
359 }
360
361 #[cfg(feature = "virtual-clients-draft")]
411 pub fn vc_emulation<Crypto: OpenMlsCrypto, Storage: StorageProvider>(
412 mut self,
413 crypto: &Crypto,
414 storage: &Storage,
415 epoch_id: EpochId,
416 ) -> Result<Self, CreateCommitError> {
417 let state: EmulationEpochState = storage
418 .vc_emulation_epoch_state(&epoch_id)
419 .map_err(|e| {
420 log::error!("vc: load emulation epoch state in vc_emulation failed: {e:?}");
421 CreateCommitError::VirtualClientsError(VirtualClientsError::StorageError)
422 })?
423 .ok_or(VirtualClientsError::MissingEmulationEpochState)?;
424 let mut operation_tree: OperationSecretTree = storage
425 .vc_operation_tree(&epoch_id)
426 .map_err(|e| {
427 log::error!("vc: load operation tree in vc_emulation failed: {e:?}");
428 CreateCommitError::VirtualClientsError(VirtualClientsError::StorageError)
429 })?
430 .ok_or(VirtualClientsError::MissingOperationTree)?;
431 let (emulation_leaf_index, epoch_encryption_key, emulation_ciphersuite) =
432 state.into_parts();
433
434 let own_leaf_index = self.group.borrow().own_leaf_index();
441 let is_external_commit = self.stage.external_commit_info.is_some();
442 let resolved_dictionary = check_vc_leaf_configuration(
443 &self.stage.leaf_node_parameters,
444 self.group.borrow(),
445 own_leaf_index,
446 is_external_commit,
447 )?;
448
449 let (generation, operation_secret) = operation_tree.next_operation_secret(
454 crypto,
455 emulation_ciphersuite,
456 &epoch_id,
457 emulation_leaf_index,
458 VirtualClientOperationType::LeafNode,
459 self.group.borrow().group_id().as_slice(),
460 )?;
461 storage
464 .write_vc_operation_tree(&epoch_id, &operation_tree)
465 .map_err(|e| {
466 log::error!("vc: persist advanced operation tree failed: {e:?}");
467 CreateCommitError::VirtualClientsError(VirtualClientsError::StorageError)
468 })?;
469
470 self.vc_loaded = Some(VcLoaded {
471 epoch_id,
472 emulation_leaf_index,
473 epoch_encryption_key,
474 emulation_ciphersuite,
475 generation,
476 operation_secret,
477 resolved_dictionary,
478 });
479 Ok(self)
480 }
481
482 pub fn load_psks<Storage: StorageProvider>(
484 self,
485 storage: &'a Storage,
486 ) -> Result<CommitBuilder<'a, LoadedPsks, G>, CreateCommitError> {
487 let psk_ids: Vec<_> = self
488 .stage
489 .own_proposals
490 .iter()
491 .chain(
492 self.group
493 .borrow()
494 .proposal_store()
495 .proposals()
496 .map(|queued_proposal| queued_proposal.proposal()),
497 )
498 .filter_map(|proposal| match proposal {
499 Proposal::PreSharedKey(psk_proposal) => Some(psk_proposal.clone().into_psk_id()),
500 _ => None,
501 })
502 .collect();
503
504 let psks = load_psks(storage, &self.group.borrow().resumption_psk_store, &psk_ids)?
506 .into_iter()
507 .map(|(psk_id_ref, key)| (psk_id_ref.clone(), key))
508 .collect();
509
510 let use_ratchet_tree_extension = self
512 .group
513 .borrow()
514 .configuration()
515 .use_ratchet_tree_extension;
516
517 let group_info_config = GroupInfoConfig {
518 use_ratchet_tree_extension,
519 create_group_info: use_ratchet_tree_extension,
520 other_extensions: vec![],
521 };
522
523 Ok(self
524 .map_stage(|stage| {
525 (
526 (),
527 LoadedPsks {
528 own_proposals: stage.own_proposals,
529 psks,
530 force_self_update: stage.force_self_update,
531 leaf_node_parameters: stage.leaf_node_parameters,
532 consume_proposal_store: stage.consume_proposal_store,
533 group_info_config,
534 external_commit_info: stage.external_commit_info,
535 #[cfg(feature = "extensions-draft")]
536 app_data_dictionary_updates: None,
537 },
538 )
539 })
540 .1)
541 }
542}
543
544impl<'a, G: BorrowMut<MlsGroup>> CommitBuilder<'a, LoadedPsks, G> {
545 pub fn create_group_info(mut self, create_group_info: bool) -> Self {
548 self.stage.group_info_config.create_group_info = create_group_info;
549 self
550 }
551
552 pub fn use_ratchet_tree_extension(mut self, use_ratchet_tree_extension: bool) -> Self {
555 if use_ratchet_tree_extension {
556 self.stage.group_info_config.create_group_info = true;
557 }
558 self.stage.group_info_config.use_ratchet_tree_extension = use_ratchet_tree_extension;
559 self
560 }
561
562 pub fn create_group_info_with_extensions(
567 mut self,
568 extensions: impl IntoIterator<Item = Extension>,
569 ) -> Result<Self, InvalidExtensionError> {
570 self.stage.group_info_config.create_group_info = true;
571 self.stage.group_info_config.other_extensions = extensions
572 .into_iter()
573 .map(|extension| {
574 if extension.as_ratchet_tree_extension().is_ok()
575 || extension.as_external_pub_extension().is_ok()
576 {
577 Err(InvalidExtensionError::CannotAddDirectlyToGroupInfo)
578 } else {
579 Ok(extension)
580 }
581 })
582 .collect::<Result<Vec<_>, _>>()?;
583
584 Ok(self)
585 }
586 pub fn build<S: Signer>(
590 self,
591 rand: &impl OpenMlsRand,
592 crypto: &impl OpenMlsCrypto,
593 signer: &S,
594 f: impl FnMut(&QueuedProposal) -> bool,
595 ) -> Result<CommitBuilder<'a, Complete, G>, CreateCommitError> {
596 self.build_internal(rand, crypto, signer, None::<NewSignerBundle<'_, S>>, f)
597 }
598
599 pub fn build_with_new_signer<S: Signer>(
607 self,
608 rand: &impl OpenMlsRand,
609 crypto: &impl OpenMlsCrypto,
610 old_signer: &impl Signer,
611 new_signer: NewSignerBundle<'_, S>,
612 f: impl FnMut(&QueuedProposal) -> bool,
613 ) -> Result<CommitBuilder<'a, Complete, G>, CreateCommitError> {
614 self.build_internal(rand, crypto, old_signer, Some(new_signer), f)
615 }
616
617 fn build_internal<S: Signer>(
618 self,
619 rand: &impl OpenMlsRand,
620 crypto: &impl OpenMlsCrypto,
621 old_signer: &impl Signer,
622 new_signer: Option<NewSignerBundle<'_, S>>,
623 f: impl FnMut(&QueuedProposal) -> bool,
624 ) -> Result<CommitBuilder<'a, Complete, G>, CreateCommitError> {
625 #[cfg_attr(not(feature = "virtual-clients-draft"), allow(unused_mut))]
626 let (mut cur_stage, mut builder) = self.take_stage();
627
628 let GroupInfoConfig {
630 create_group_info,
631 use_ratchet_tree_extension,
632 other_extensions,
633 } = cur_stage.group_info_config;
634
635 let group = builder.group.borrow();
636 let ciphersuite = group.ciphersuite();
637 let own_leaf_index = group.own_leaf_index();
638 let (sender, is_external_commit) = match cur_stage.external_commit_info {
639 None => (Sender::build_member(own_leaf_index), false),
640 Some(_) => (Sender::NewMemberCommit, true),
641 };
642 let psks = cur_stage.psks;
643
644 let own_proposals: Vec<_> = cur_stage
647 .own_proposals
648 .into_iter()
649 .map(|proposal| {
650 QueuedProposal::from_proposal_and_sender(ciphersuite, crypto, proposal, &sender)
651 })
652 .collect::<Result<_, _>>()?;
653
654 let group_proposal_store_queue = group
657 .pending_proposals()
658 .filter(|_| cur_stage.consume_proposal_store)
659 .cloned();
660
661 let proposal_queue = group_proposal_store_queue.chain(own_proposals).filter(f);
665
666 let (proposal_queue, contains_own_updates) =
667 ProposalQueue::filter_proposals(proposal_queue, group.own_leaf_index).map_err(|e| {
668 match e {
669 ProposalQueueError::LibraryError(e) => e.into(),
670 ProposalQueueError::ProposalNotFound => CreateCommitError::MissingProposal,
671 ProposalQueueError::UpdateFromExternalSender
672 | ProposalQueueError::SelfRemoveFromNonMember => {
673 CreateCommitError::WrongProposalSenderType
674 }
675 }
676 })?;
677
678 group
683 .public_group
684 .validate_proposal_type_support(&proposal_queue)?;
685 group
690 .public_group
691 .validate_key_uniqueness(&proposal_queue, None)?;
692 group.public_group.validate_add_proposals(&proposal_queue)?;
694 group.public_group.validate_capabilities(&proposal_queue)?;
697 group
700 .public_group
701 .validate_remove_proposals(&proposal_queue)?;
702 group
703 .public_group
704 .validate_pre_shared_key_proposals(&proposal_queue)?;
705 group
710 .public_group
711 .validate_update_proposals(&proposal_queue, own_leaf_index)?;
712
713 group
716 .public_group
717 .validate_group_context_extensions_proposal(&proposal_queue)?;
718
719 #[cfg(feature = "extensions-draft")]
720 group
721 .public_group
722 .validate_app_data_update_proposals_and_group_context(&proposal_queue)?;
723
724 if is_external_commit {
725 group
726 .public_group
727 .validate_external_commit(&proposal_queue)?;
728 }
729
730 let proposal_reference_list = proposal_queue.commit_list();
731
732 let mut diff = group.public_group.empty_diff();
734
735 #[cfg(feature = "extensions-draft")]
737 let apply_proposals_values = diff.apply_proposals_with_app_data_updates(
738 &proposal_queue,
739 own_leaf_index,
740 cur_stage.app_data_dictionary_updates,
741 )?;
742 #[cfg(not(feature = "extensions-draft"))]
743 let apply_proposals_values = diff.apply_proposals(&proposal_queue, own_leaf_index)?;
744 if apply_proposals_values.self_removed && !is_external_commit {
745 return Err(CreateCommitError::CannotRemoveSelf);
746 }
747
748 #[cfg(feature = "virtual-clients-draft")]
756 let vc_loaded = builder.vc_loaded.take();
757 #[cfg(feature = "virtual-clients-draft")]
758 let own_update_override = if let Some(loaded) = vc_loaded.as_ref() {
759 let external_init_secret =
769 is_external_commit.then(|| group.group_epoch_secrets().init_secret());
770 Some(apply_vc_emulation(
771 loaded,
772 &mut cur_stage.leaf_node_parameters,
773 loaded.resolved_dictionary.clone(),
774 crypto,
775 ciphersuite,
776 group.group_id(),
777 external_init_secret,
778 )?)
779 } else {
780 None
781 };
782 #[cfg(not(feature = "virtual-clients-draft"))]
783 let own_update_override: Option<crate::treesync::diff::OwnUpdatePathOverride> = None;
784
785 let path_computation_result =
786 if apply_proposals_values.path_required
788 || contains_own_updates
789 || cur_stage.force_self_update
790 || !cur_stage.leaf_node_parameters.is_empty()
791 {
792 let commit_type = match &cur_stage.external_commit_info {
793 Some(ExternalCommitInfo { credential , ..}) => {
794 CommitType::External(credential.clone())
795 }
796 None => CommitType::Member,
797 };
798 if let Some(new_signer) = new_signer {
802 if let Some(credential_with_key) =
803 cur_stage.leaf_node_parameters.credential_with_key()
804 {
805 if credential_with_key != &new_signer.credential_with_key {
806 return Err(CreateCommitError::InvalidLeafNodeParameters);
807 }
808 }
809 cur_stage.leaf_node_parameters.set_credential_with_key(
810 new_signer.credential_with_key,
811 );
812
813 diff.compute_path(
814 rand,
815 crypto,
816 own_leaf_index,
817 apply_proposals_values.exclusion_list(),
818 &commit_type,
819 &cur_stage.leaf_node_parameters,
820 new_signer.signer,
821 apply_proposals_values.extensions.clone(),
822 own_update_override,
823 )?
824 } else {
825 diff.compute_path(
826 rand,
827 crypto,
828 own_leaf_index,
829 apply_proposals_values.exclusion_list(),
830 &commit_type,
831 &cur_stage.leaf_node_parameters,
832 old_signer,
833 apply_proposals_values.extensions.clone(),
834 own_update_override,
835 )?
836 }
837 } else {
838 diff.update_group_context(crypto, apply_proposals_values.extensions.clone())?;
841 PathComputationResult::default()
842 };
843
844 let update_path_leaf_node = path_computation_result
845 .encrypted_path
846 .as_ref()
847 .map(|path| path.leaf_node().clone());
848
849 if let Some(ref leaf_node) = update_path_leaf_node {
851 if !diff
857 .group_context()
858 .extensions()
859 .iter()
860 .map(Extension::extension_type)
861 .all(|ext_type| leaf_node.supports_extension(&ext_type))
862 {
863 return Err(CreateCommitError::LeafNodeValidation(
864 LeafNodeValidationError::UnsupportedExtensions,
865 ));
866 }
867
868 if let Some(required_capabilities) =
871 diff.group_context().extensions().required_capabilities()
872 {
873 leaf_node
874 .capabilities()
875 .supports_required_capabilities(required_capabilities)?
876 }
877 }
878
879 let commit = Commit {
881 proposals: proposal_reference_list,
882 path: path_computation_result.encrypted_path,
883 };
884
885 let (outgoing_aad, wire_format): (Vec<u8>, WireFormat) =
886 match &cur_stage.external_commit_info {
887 None => (
888 group.outgoing_authenticated_data()?,
889 group.outgoing_wire_format(),
890 ),
891 Some(ExternalCommitInfo { aad, .. }) => {
892 #[cfg(feature = "extensions-draft")]
896 let aad_bytes = if group.context().safe_aad_required() {
897 crate::framing::safe_aad::assemble_authenticated_data(
898 &crate::framing::SafeAad::empty(),
899 aad,
900 )
901 .map_err(|_| LibraryError::custom("SafeAad serialization failed"))?
902 } else {
903 aad.clone()
904 };
905 #[cfg(not(feature = "extensions-draft"))]
906 let aad_bytes = aad.clone();
907 (aad_bytes, WireFormat::PublicMessage)
908 }
909 };
910 let framing_parameters = FramingParameters::new(&outgoing_aad, wire_format);
911
912 let mut authenticated_content = AuthenticatedContent::commit(
914 framing_parameters,
915 sender,
916 commit,
917 group.public_group.group_context(),
918 old_signer,
919 )?;
920
921 diff.update_confirmed_transcript_hash(crypto, &authenticated_content)?;
923
924 let serialized_provisional_group_context = diff
925 .group_context()
926 .tls_serialize_detached()
927 .map_err(LibraryError::missing_bound_check)?;
928
929 let joiner_secret = JoinerSecret::new(
930 crypto,
931 ciphersuite,
932 path_computation_result.commit_secret,
933 group.group_epoch_secrets().init_secret(),
934 &serialized_provisional_group_context,
935 )
936 .map_err(LibraryError::unexpected_crypto_error)?;
937
938 let psk_secret = { PskSecret::new(crypto, ciphersuite, psks)? };
940
941 let mut key_schedule = KeySchedule::init(ciphersuite, crypto, &joiner_secret, psk_secret)?;
943
944 let serialized_provisional_group_context = diff
945 .group_context()
946 .tls_serialize_detached()
947 .map_err(LibraryError::missing_bound_check)?;
948
949 let welcome_secret = key_schedule
950 .welcome(crypto, ciphersuite)
951 .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
952 key_schedule
953 .add_context(crypto, &serialized_provisional_group_context)
954 .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
955 let EpochSecretsResult {
956 epoch_secrets: provisional_epoch_secrets,
957 #[cfg(feature = "extensions-draft")]
958 application_exporter,
959 } = key_schedule
960 .epoch_secrets(crypto, ciphersuite)
961 .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
962
963 let confirmation_tag = provisional_epoch_secrets
965 .confirmation_key()
966 .tag(
967 crypto,
968 ciphersuite,
969 diff.group_context().confirmed_transcript_hash(),
970 )
971 .map_err(LibraryError::unexpected_crypto_error)?;
972
973 authenticated_content.set_confirmation_tag(confirmation_tag.clone());
975
976 diff.update_interim_transcript_hash(ciphersuite, crypto, confirmation_tag.clone())?;
977
978 let needs_welcome = !apply_proposals_values.invitation_list.is_empty();
980
981 let needs_group_info = needs_welcome || create_group_info;
985
986 let (welcome_option, group_info) = if !needs_group_info {
987 (None, None)
988 } else {
989 let mut extensions_list = vec![];
991 if use_ratchet_tree_extension {
992 extensions_list.push(Extension::RatchetTree(RatchetTreeExtension::new(
993 diff.export_ratchet_tree(),
994 )));
995 };
996 extensions_list.extend(other_extensions);
998
999 let mut extensions = Extensions::from_vec(extensions_list)?;
1000
1001 let welcome_option = needs_welcome
1002 .then(|| -> Result<_, CreateCommitError> {
1003 let group_info_tbs = {
1004 GroupInfoTBS::new(
1005 diff.group_context().clone(),
1006 extensions.clone(),
1007 confirmation_tag.clone(),
1008 own_leaf_index,
1009 )?
1010 };
1011 let group_info = group_info_tbs.sign(old_signer)?;
1013
1014 let (welcome_key, welcome_nonce) = welcome_secret
1016 .derive_welcome_key_nonce(crypto, ciphersuite)
1017 .map_err(LibraryError::unexpected_crypto_error)?;
1018 let encrypted_group_info = welcome_key
1019 .aead_seal(
1020 crypto,
1021 group_info
1022 .tls_serialize_detached()
1023 .map_err(LibraryError::missing_bound_check)?
1024 .as_slice(),
1025 &[],
1026 &welcome_nonce,
1027 )
1028 .map_err(LibraryError::unexpected_crypto_error)?;
1029
1030 let encrypted_secrets = diff.encrypt_group_secrets(
1033 &joiner_secret,
1034 apply_proposals_values.invitation_list,
1035 path_computation_result.plain_path.as_deref(),
1036 &apply_proposals_values.presharedkeys,
1037 &encrypted_group_info,
1038 crypto,
1039 own_leaf_index,
1040 )?;
1041
1042 let welcome =
1044 Welcome::new(ciphersuite, encrypted_secrets, encrypted_group_info);
1045 Ok(welcome)
1046 })
1047 .transpose()?;
1048
1049 let exported_group_info = create_group_info
1052 .then(|| -> Result<_, CreateCommitError> {
1053 let external_pub = provisional_epoch_secrets
1054 .external_secret()
1055 .derive_external_keypair(crypto, ciphersuite)
1056 .map_err(LibraryError::unexpected_crypto_error)?
1057 .public;
1058
1059 let external_pub_extension =
1060 Extension::ExternalPub(ExternalPubExtension::new(external_pub.into()));
1061 extensions.add(external_pub_extension)?;
1062 let group_info_tbs = {
1063 GroupInfoTBS::new(
1064 diff.group_context().clone(),
1065 extensions,
1066 confirmation_tag.clone(),
1067 own_leaf_index,
1068 )?
1069 };
1070 Ok(group_info_tbs.sign(old_signer)?)
1072 })
1073 .transpose()?;
1074
1075 (welcome_option, exported_group_info)
1076 };
1077
1078 let (provisional_group_epoch_secrets, provisional_message_secrets) =
1079 provisional_epoch_secrets.split_secrets(
1080 serialized_provisional_group_context,
1081 diff.tree_size(),
1082 own_leaf_index,
1083 );
1084
1085 #[cfg(feature = "extensions-draft")]
1086 let application_export_tree = ApplicationExportTree::new(application_exporter);
1087 let staged_commit_state = MemberStagedCommitState::new(
1088 provisional_group_epoch_secrets,
1089 provisional_message_secrets,
1090 diff.into_staged_diff(crypto, ciphersuite)?,
1091 path_computation_result.new_keypairs,
1092 None,
1095 update_path_leaf_node,
1096 #[cfg(feature = "extensions-draft")]
1097 application_export_tree,
1098 #[cfg(feature = "virtual-clients-draft")]
1102 None,
1103 );
1104 let staged_commit = StagedCommit::new(
1105 proposal_queue,
1106 StagedCommitState::GroupMember(Box::new(staged_commit_state)),
1107 #[cfg(feature = "virtual-clients-draft")]
1108 vc_loaded.as_ref().map(|loaded| loaded.epoch_id.clone()),
1109 );
1110
1111 Ok(builder.into_stage(Complete {
1112 result: CreateCommitResult {
1113 commit: authenticated_content,
1114 welcome_option,
1115 staged_commit,
1116 group_info: group_info.filter(|_| create_group_info),
1117 },
1118 original_wire_format_policy: cur_stage
1119 .external_commit_info
1120 .as_ref()
1121 .map(|info| info.wire_format_policy),
1122 }))
1123 }
1124
1125 #[cfg(feature = "extensions-draft")]
1130 pub fn app_data_dictionary_updater(&self) -> AppDataDictionaryUpdater<'_> {
1131 AppDataDictionaryUpdater::new(self.group.borrow().context().app_data_dict())
1132 }
1133
1134 #[cfg(feature = "extensions-draft")]
1136 pub fn with_app_data_dictionary_updates(
1137 &mut self,
1138 app_data_dictionary_updates: Option<AppDataUpdates>,
1139 ) {
1140 self.stage.app_data_dictionary_updates = app_data_dictionary_updates;
1141 }
1142
1143 #[cfg(feature = "extensions-draft")]
1145 pub fn app_data_update_proposals(&self) -> impl Iterator<Item = &AppDataUpdateProposal> {
1146 let proposal_store_proposals = self
1147 .group
1148 .borrow()
1149 .proposal_store()
1150 .proposals()
1151 .map(|queued_proposal| queued_proposal.proposal());
1152
1153 let all_proposals = proposal_store_proposals.chain(self.stage.own_proposals.iter());
1155
1156 let mut app_data_update_proposals: Vec<&AppDataUpdateProposal> = all_proposals
1158 .filter_map(|proposal| match proposal {
1159 Proposal::AppDataUpdate(proposal) => Some(proposal.as_ref()),
1160 _ => None,
1161 })
1162 .collect();
1163
1164 app_data_update_proposals.sort_by_key(|prop| prop.component_id());
1165 app_data_update_proposals.into_iter()
1166 }
1167}
1168
1169impl CommitBuilder<'_, Complete, &mut MlsGroup> {
1171 #[cfg(test)]
1172 pub(crate) fn commit_result(self) -> CreateCommitResult {
1173 self.stage.result
1174 }
1175
1176 pub fn stage_commit<Provider: OpenMlsProvider>(
1178 self,
1179 provider: &Provider,
1180 ) -> Result<CommitMessageBundle, CommitBuilderStageError<Provider::StorageError>> {
1181 let Self {
1182 group,
1183 stage:
1184 Complete {
1185 result: create_commit_result,
1186 original_wire_format_policy: _,
1187 },
1188 ..
1189 } = self;
1190
1191 group.group_state = MlsGroupState::PendingCommit(Box::new(PendingCommitState::Member(
1194 create_commit_result.staged_commit,
1195 )));
1196
1197 provider
1198 .storage()
1199 .write_group_state(group.group_id(), &group.group_state)
1200 .map_err(CommitBuilderStageError::KeyStoreError)?;
1201
1202 group.reset_aad();
1203
1204 let framing = group.content_to_mls_message(create_commit_result.commit, provider)?;
1210
1211 Ok(CommitMessageBundle {
1212 version: group.version(),
1213 commit: framing.message,
1214 welcome: create_commit_result.welcome_option,
1215 group_info: create_commit_result.group_info,
1216 #[cfg(feature = "virtual-clients-draft")]
1217 confirmation: framing.confirmation,
1218 })
1219 }
1220}
1221
1222#[cfg(feature = "virtual-clients-draft")]
1234fn apply_vc_emulation(
1235 loaded: &VcLoaded,
1236 leaf_node_parameters: &mut LeafNodeParameters,
1237 resolved_dictionary: AppDataDictionary,
1238 crypto: &impl OpenMlsCrypto,
1239 group_ciphersuite: openmls_traits::types::Ciphersuite,
1240 group_id: &crate::prelude::GroupId,
1241 external_init_secret: Option<&crate::schedule::InitSecret>,
1242) -> Result<crate::treesync::diff::OwnUpdatePathOverride, CreateCommitError> {
1243 let target_operation_secret = loaded.operation_secret.derive_target_operation_secret(
1244 crypto,
1245 group_ciphersuite,
1246 group_id,
1247 )?;
1248 let path_secret = target_operation_secret
1249 .derive_path_generation_secret(crypto, group_ciphersuite)?
1250 .into();
1251 let leaf_encryption_keypair = target_operation_secret
1252 .derive_encryption_key_secret(crypto, group_ciphersuite)?
1253 .generate_encryption_key_pair(crypto, group_ciphersuite)?;
1254 drop(target_operation_secret);
1255
1256 let leaf_encryption_key = leaf_encryption_keypair
1259 .public_key()
1260 .tls_serialize_detached()
1261 .map_err(VirtualClientsError::from)?;
1262 let tbe = DerivationInfoTbe::LeafNode {
1266 leaf_index: loaded.emulation_leaf_index,
1267 generation: loaded.generation,
1268 external_init_secret: external_init_secret
1269 .map(|init_secret| ExternalInitSecret::from_slice(init_secret.as_slice())),
1270 };
1271 let derivation_info = DerivationInfo::encrypt(
1272 crypto,
1273 loaded.emulation_ciphersuite,
1274 &loaded.epoch_encryption_key,
1275 loaded.epoch_id.clone(),
1276 &leaf_encryption_key,
1277 &tbe,
1278 )?;
1279 let derivation_info_bytes = derivation_info
1280 .tls_serialize_detached()
1281 .map_err(VirtualClientsError::from)?;
1282
1283 inject_vc_derivation_info(
1284 leaf_node_parameters,
1285 resolved_dictionary,
1286 derivation_info_bytes,
1287 )?;
1288
1289 Ok(crate::treesync::diff::OwnUpdatePathOverride {
1290 path_secret,
1291 leaf_encryption_keypair,
1292 })
1293}
1294
1295#[cfg(feature = "virtual-clients-draft")]
1307fn check_vc_leaf_configuration(
1308 leaf_node_parameters: &LeafNodeParameters,
1309 group: &MlsGroup,
1310 own_leaf_index: LeafNodeIndex,
1311 is_external_commit: bool,
1312) -> Result<AppDataDictionary, CreateCommitError> {
1313 let current_leaf = if is_external_commit {
1314 None
1315 } else {
1316 Some(group.public_group().leaf(own_leaf_index).ok_or_else(|| {
1317 LibraryError::custom("Couldn't find own leaf for VC capability check")
1318 })?)
1319 };
1320
1321 crate::components::vc_derivation_info::resolve_vc_leaf_dictionary(
1322 leaf_node_parameters.capabilities(),
1323 leaf_node_parameters.extensions(),
1324 current_leaf,
1325 )
1326 .map_err(CreateCommitError::VirtualClientsError)
1327}
1328
1329#[cfg(feature = "virtual-clients-draft")]
1334fn inject_vc_derivation_info(
1335 leaf_node_parameters: &mut LeafNodeParameters,
1336 resolved_dictionary: AppDataDictionary,
1337 derivation_info_bytes: Vec<u8>,
1338) -> Result<(), CreateCommitError> {
1339 let extensions = crate::components::vc_derivation_info::merge_vc_derivation_info(
1340 leaf_node_parameters.extensions(),
1341 resolved_dictionary,
1342 derivation_info_bytes,
1343 )?;
1344 leaf_node_parameters.set_extensions(extensions);
1345 Ok(())
1346}
1347
1348#[derive(Debug, Clone)]
1351pub struct CommitMessageBundle {
1352 version: ProtocolVersion,
1353 commit: MlsMessageOut,
1354 welcome: Option<Welcome>,
1355 group_info: Option<GroupInfo>,
1356 #[cfg(feature = "virtual-clients-draft")]
1359 confirmation: Option<HandshakeConfirmationData>,
1360}
1361
1362pub struct WelcomeCommitMessages {
1367 pub commit: MlsMessageOut,
1369
1370 pub welcome: MlsMessageOut,
1372
1373 pub group_info: Option<MlsMessageOut>,
1375}
1376
1377impl TryFrom<CommitMessageBundle> for WelcomeCommitMessages {
1378 type Error = LibraryError;
1379
1380 fn try_from(value: CommitMessageBundle) -> Result<Self, Self::Error> {
1381 let (commit, welcome_opt, group_info) = value.into_messages();
1382 Ok(Self {
1383 commit,
1384 welcome: welcome_opt.ok_or_else(|| {
1385 LibraryError::custom(
1386 "WelcomeCommitMessages must only be used with commits that produce a welcome.",
1387 )
1388 })?,
1389 group_info,
1390 })
1391 }
1392}
1393
1394#[cfg(test)]
1395impl CommitMessageBundle {
1396 pub fn new(
1397 version: ProtocolVersion,
1398 commit: MlsMessageOut,
1399 welcome: Option<Welcome>,
1400 group_info: Option<GroupInfo>,
1401 ) -> Self {
1402 Self {
1403 version,
1404 commit,
1405 welcome,
1406 group_info,
1407 #[cfg(feature = "virtual-clients-draft")]
1408 confirmation: None,
1409 }
1410 }
1411}
1412
1413impl CommitMessageBundle {
1414 pub fn commit(&self) -> &MlsMessageOut {
1418 &self.commit
1419 }
1420
1421 pub fn welcome(&self) -> Option<&Welcome> {
1424 self.welcome.as_ref()
1425 }
1426
1427 pub fn to_welcome_msg(&self) -> Option<MlsMessageOut> {
1430 self.welcome
1431 .as_ref()
1432 .map(|welcome| MlsMessageOut::from_welcome(welcome.clone(), self.version))
1433 }
1434
1435 pub fn group_info(&self) -> Option<&GroupInfo> {
1439 self.group_info.as_ref()
1440 }
1441
1442 #[cfg(feature = "virtual-clients-draft")]
1450 pub fn confirmation(&self) -> Option<&HandshakeConfirmationData> {
1451 self.confirmation.as_ref()
1452 }
1453
1454 #[cfg(feature = "virtual-clients-draft")]
1460 pub fn take_confirmation(&mut self) -> Option<HandshakeConfirmationData> {
1461 self.confirmation.take()
1462 }
1463
1464 pub fn contents(&self) -> (&MlsMessageOut, Option<&Welcome>, Option<&GroupInfo>) {
1467 (
1468 &self.commit,
1469 self.welcome.as_ref(),
1470 self.group_info.as_ref(),
1471 )
1472 }
1473
1474 pub fn into_commit(self) -> MlsMessageOut {
1478 self.commit
1479 }
1480
1481 pub fn into_welcome(self) -> Option<Welcome> {
1485 self.welcome
1486 }
1487
1488 pub fn into_welcome_msg(self) -> Option<MlsMessageOut> {
1491 self.welcome
1492 .map(|welcome| MlsMessageOut::from_welcome(welcome, self.version))
1493 }
1494
1495 pub fn into_group_info(self) -> Option<GroupInfo> {
1500 self.group_info
1501 }
1502
1503 pub fn into_group_info_msg(self) -> Option<MlsMessageOut> {
1505 self.group_info.map(|group_info| group_info.into())
1506 }
1507
1508 pub fn into_contents(self) -> (MlsMessageOut, Option<Welcome>, Option<GroupInfo>) {
1511 (self.commit, self.welcome, self.group_info)
1512 }
1513
1514 pub fn into_messages(self) -> (MlsMessageOut, Option<MlsMessageOut>, Option<MlsMessageOut>) {
1517 (
1518 self.commit,
1519 self.welcome
1520 .map(|welcome| MlsMessageOut::from_welcome(welcome, self.version)),
1521 self.group_info.map(|group_info| group_info.into()),
1522 )
1523 }
1524}
1525
1526impl IntoIterator for CommitMessageBundle {
1527 type Item = MlsMessageOut;
1528
1529 type IntoIter = core::iter::Chain<
1530 core::iter::Chain<
1531 core::option::IntoIter<MlsMessageOut>,
1532 core::option::IntoIter<MlsMessageOut>,
1533 >,
1534 core::option::IntoIter<MlsMessageOut>,
1535 >;
1536
1537 fn into_iter(self) -> Self::IntoIter {
1538 let welcome = self.to_welcome_msg();
1539 let group_info = self.group_info.map(|group_info| group_info.into());
1540
1541 Some(self.commit)
1542 .into_iter()
1543 .chain(welcome)
1544 .chain(group_info)
1545 }
1546}