1use core::fmt::Debug;
2
3use openmls_traits::crypto::OpenMlsCrypto;
4use openmls_traits::storage::StorageProvider as _;
5use serde::{Deserialize, Serialize};
6use tls_codec::Serialize as _;
7
8use super::proposal_store::{
9 QueuedAddProposal, QueuedPskProposal, QueuedRemoveProposal, QueuedUpdateProposal,
10};
11
12#[cfg(feature = "virtual-clients-draft")]
13use super::Sender;
14#[cfg(feature = "extensions-draft")]
15use super::StagedCommitSafeExport;
16use super::{
17 super::errors::*, load_psks, Credential, ExportedSecret, Extension, GroupContext,
18 GroupEpochSecrets, GroupId, JoinerSecret, KeySchedule, LeafNode, LibraryError, MessageSecrets,
19 MlsGroup, MlsGroupState, OpenMlsProvider, PendingCommitState, Proposal, ProposalQueue,
20 PskSecret, QueuedProposal, ReInitProposal, StagedCommitExport,
21};
22use crate::group::diff::PublicGroupDiff;
23use crate::group::GroupEpoch;
24use crate::messages::ConfirmationTag;
25use crate::prelude::{Commit, LeafNodeIndex};
26#[cfg(feature = "extensions-draft")]
27use crate::{component::ComponentId, schedule::application_export_tree::ApplicationExportTree};
28
29use crate::treesync::errors::TreeSyncFromNodesError;
30use crate::treesync::RatchetTree;
31use crate::{
32 ciphersuite::{hash_ref::ProposalRef, Secret},
33 framing::mls_auth_content::AuthenticatedContent,
34 group::public_group::{
35 diff::{apply_proposals::ApplyProposalsValues, StagedPublicGroupDiff},
36 staged_commit::PublicStagedCommitState,
37 },
38 schedule::{
39 CommitSecret, EpochAuthenticator, EpochSecretsResult, InitSecret, PreSharedKeyId,
40 ResumptionPskSecret,
41 },
42 treesync::node::encryption_keys::EncryptionKeyPair,
43};
44
45#[cfg(feature = "extensions-draft")]
46use super::proposal_store::{QueuedAppDataUpdateProposal, QueuedAppEphemeralProposal};
47#[cfg(feature = "extensions-draft")]
48use crate::prelude::processing::AppDataUpdates;
49
50#[cfg(feature = "virtual-clients-draft")]
51fn validate_vc_external_init_secret(
52 is_sibling_resync: bool,
53 has_external_init_proposal: bool,
54 has_vc_external_init_secret: bool,
55) -> Result<(), StageCommitError> {
56 if is_sibling_resync && !has_vc_external_init_secret {
57 return Err(
58 crate::components::vc_derivation_info::VirtualClientsError::DerivationInfoMalformed
59 .into(),
60 );
61 }
62 if has_vc_external_init_secret && !has_external_init_proposal {
63 return Err(
64 crate::components::vc_derivation_info::VirtualClientsError::DerivationInfoMalformed
65 .into(),
66 );
67 }
68 Ok(())
69}
70
71#[cfg(feature = "virtual-clients-draft")]
79fn commit_marks_new_vc_derivation_epoch(
80 authenticated_data: &[u8],
81) -> Result<bool, StageCommitError> {
82 use crate::components::vc_commit_data::VirtualClientCommitData;
83
84 let (safe_aad, _prefix_len) =
85 crate::framing::safe_aad::parse_authenticated_data_prefix(authenticated_data)
86 .map_err(|e| StageCommitError::MalformedVcCommitData(e.to_string()))?;
87 let commit_data = VirtualClientCommitData::from_safe_aad(&safe_aad)
88 .map_err(|e| StageCommitError::MalformedVcCommitData(e.to_string()))?;
89 Ok(commit_data.is_some_and(|commit_data| commit_data.creates_derivation_epoch()))
90}
91
92impl MlsGroup {
93 pub(crate) fn matches_pending_commit(&self, received_tag: &ConfirmationTag) -> bool {
102 let MlsGroupState::PendingCommit(pending_commit_state) = &self.group_state else {
103 return false;
104 };
105 let PendingCommitState::Member(staged_commit) = pending_commit_state.as_ref() else {
106 return false;
107 };
108 let StagedCommitState::GroupMember(member_state) = &staged_commit.state else {
109 return false;
110 };
111 member_state.staged_diff.confirmation_tag() == received_tag
112 }
113
114 fn derive_epoch_secrets(
115 &self,
116 provider: &impl OpenMlsProvider,
117 apply_proposals_values: ApplyProposalsValues,
118 epoch_secrets: &GroupEpochSecrets,
119 commit_secret: CommitSecret,
120 serialized_provisional_group_context: &[u8],
121 #[cfg(feature = "virtual-clients-draft")] vc_external_init_secret: Option<
122 &crate::components::vc_derivation_info::ExternalInitSecret,
123 >,
124 ) -> Result<EpochSecretsResult, StageCommitError> {
125 let joiner_secret = if let Some(ref external_init_proposal) =
128 apply_proposals_values.external_init_proposal_option
129 {
130 #[cfg(feature = "virtual-clients-draft")]
137 let carried_init_secret = vc_external_init_secret
138 .map(|carried| InitSecret::from(Secret::from_slice(carried.as_slice())));
139 #[cfg(not(feature = "virtual-clients-draft"))]
140 let carried_init_secret: Option<InitSecret> = None;
141
142 let init_secret = match carried_init_secret {
143 Some(init_secret) => init_secret,
144 None => {
145 let external_priv = epoch_secrets
146 .external_secret()
147 .derive_external_keypair(provider.crypto(), self.ciphersuite())
148 .map_err(LibraryError::unexpected_crypto_error)?
149 .private;
150 InitSecret::from_kem_output(
151 provider.crypto(),
152 self.ciphersuite(),
153 self.version(),
154 &external_priv,
155 external_init_proposal.kem_output(),
156 )?
157 }
158 };
159 JoinerSecret::new(
160 provider.crypto(),
161 self.ciphersuite(),
162 commit_secret,
163 &init_secret,
164 serialized_provisional_group_context,
165 )
166 .map_err(LibraryError::unexpected_crypto_error)?
167 } else {
168 JoinerSecret::new(
169 provider.crypto(),
170 self.ciphersuite(),
171 commit_secret,
172 epoch_secrets.init_secret(),
173 serialized_provisional_group_context,
174 )
175 .map_err(LibraryError::unexpected_crypto_error)?
176 };
177
178 let psk_secret = {
181 let psks: Vec<(&PreSharedKeyId, Secret)> = load_psks(
182 provider.storage(),
183 &self.resumption_psk_store,
184 &apply_proposals_values.presharedkeys,
185 )?;
186
187 PskSecret::new(provider.crypto(), self.ciphersuite(), psks)?
188 };
189
190 let mut key_schedule = KeySchedule::init(
192 self.ciphersuite(),
193 provider.crypto(),
194 &joiner_secret,
195 psk_secret,
196 )?;
197
198 key_schedule
199 .add_context(provider.crypto(), serialized_provisional_group_context)
200 .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
201 Ok(key_schedule
202 .epoch_secrets(provider.crypto(), self.ciphersuite())
203 .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?)
204 }
205
206 pub(crate) fn stage_commit(
245 &self,
246 mls_content: &AuthenticatedContent,
247 old_epoch_keypairs: Vec<EncryptionKeyPair>,
248 leaf_node_keypairs: Vec<EncryptionKeyPair>,
249 provider: &impl OpenMlsProvider,
250 #[cfg(feature = "virtual-clients-draft")] vc_commit_material: Option<
251 crate::components::vc_derivation_info::VcCommitMaterial,
252 >,
253 ) -> Result<StagedCommit, StageCommitError> {
254 let (commit, proposal_queue, sender_index) = self
255 .public_group
256 .validate_commit(mls_content, provider.crypto())?;
257
258 let mut diff = self.public_group.empty_diff();
261
262 #[cfg(not(feature = "extensions-draft"))]
263 let apply_proposals_values =
264 diff.apply_proposals(&proposal_queue, self.own_leaf_index())?;
265
266 #[cfg(feature = "extensions-draft")]
267 let apply_proposals_values = diff.apply_proposals_with_app_data_updates(
268 &proposal_queue,
269 self.own_leaf_index(),
270 None,
271 )?;
272 self.stage_applied_proposal_values(
273 apply_proposals_values,
274 diff,
275 commit,
276 proposal_queue,
277 sender_index,
278 mls_content,
279 old_epoch_keypairs,
280 leaf_node_keypairs,
281 provider,
282 #[cfg(feature = "virtual-clients-draft")]
283 vc_commit_material,
284 )
285 }
286
287 #[cfg(feature = "extensions-draft")]
288 #[allow(clippy::too_many_arguments)]
289 pub(crate) fn stage_commit_with_app_data_updates(
290 &self,
291 mls_content: &AuthenticatedContent,
292 old_epoch_keypairs: Vec<EncryptionKeyPair>,
293 leaf_node_keypairs: Vec<EncryptionKeyPair>,
294 app_data_dict_updates: Option<AppDataUpdates>,
295 provider: &impl OpenMlsProvider,
296 #[cfg(feature = "virtual-clients-draft")] vc_commit_material: Option<
297 crate::components::vc_derivation_info::VcCommitMaterial,
298 >,
299 ) -> Result<StagedCommit, StageCommitError> {
300 let (commit, proposal_queue, sender_index) = self
301 .public_group
302 .validate_commit(mls_content, provider.crypto())?;
303
304 let mut diff = self.public_group.empty_diff();
307
308 let apply_proposals_values = diff.apply_proposals_with_app_data_updates(
309 &proposal_queue,
310 self.own_leaf_index(),
311 app_data_dict_updates,
312 )?;
313
314 self.stage_applied_proposal_values(
315 apply_proposals_values,
316 diff,
317 commit,
318 proposal_queue,
319 sender_index,
320 mls_content,
321 old_epoch_keypairs,
322 leaf_node_keypairs,
323 provider,
324 #[cfg(feature = "virtual-clients-draft")]
325 vc_commit_material,
326 )
327 }
328
329 #[allow(clippy::too_many_arguments)]
330 fn stage_applied_proposal_values(
331 &self,
332 apply_proposals_values: ApplyProposalsValues,
333 mut diff: PublicGroupDiff,
334 commit: &Commit,
335 proposal_queue: ProposalQueue,
336 sender_index: LeafNodeIndex,
337 mls_content: &AuthenticatedContent,
338 old_epoch_keypairs: Vec<EncryptionKeyPair>,
339 leaf_node_keypairs: Vec<EncryptionKeyPair>,
340 provider: &impl OpenMlsProvider,
341 #[cfg(feature = "virtual-clients-draft")] vc_commit_material: Option<
342 crate::components::vc_derivation_info::VcCommitMaterial,
343 >,
344 ) -> Result<StagedCommit, StageCommitError> {
345 let ciphersuite = self.ciphersuite();
346
347 #[cfg(feature = "virtual-clients-draft")]
350 let marks_new_vc_derivation_epoch =
351 if self.is_emulation_group() && self.context().safe_aad_required() {
352 commit_marks_new_vc_derivation_epoch(mls_content.authenticated_data())?
353 } else {
354 false
355 };
356
357 #[cfg(feature = "virtual-clients-draft")]
362 let (vc_material, vc_derivation_epoch_id, vc_external_init_secret) =
363 match vc_commit_material {
364 Some(material) => (
365 Some(material.operation_secret),
366 Some(material.epoch_id),
367 material.external_init_secret,
368 ),
369 None => (None, None, None),
370 };
371
372 #[cfg(feature = "virtual-clients-draft")]
393 let is_sibling_resync =
394 vc_material.is_some() && matches!(mls_content.sender(), Sender::NewMemberCommit);
395 #[cfg(not(feature = "virtual-clients-draft"))]
396 let is_sibling_resync = false;
397
398 #[cfg(feature = "virtual-clients-draft")]
403 validate_vc_external_init_secret(
404 is_sibling_resync,
405 apply_proposals_values
406 .external_init_proposal_option
407 .is_some(),
408 vc_external_init_secret.is_some(),
409 )?;
410
411 let (commit_secret, new_keypairs, new_leaf_keypair_option, update_path_leaf_node) =
413 if let Some(path) = commit.path.clone() {
414 diff.apply_received_update_path(
417 provider.crypto(),
418 ciphersuite,
419 sender_index,
420 &path,
421 )?;
422
423 diff.update_group_context(
425 provider.crypto(),
426 apply_proposals_values.extensions.clone(),
427 )?;
428
429 if apply_proposals_values.self_removed && !is_sibling_resync {
435 let staged_diff = diff.into_staged_diff(provider.crypto(), ciphersuite)?;
437 let staged_state = PublicStagedCommitState::new(
438 staged_diff,
439 commit.path.as_ref().map(|path| path.leaf_node().clone()),
440 );
441 let staged_commit = StagedCommit::new(
442 proposal_queue,
443 StagedCommitState::PublicState(Box::new(staged_state)),
444 #[cfg(feature = "virtual-clients-draft")]
445 None,
446 );
447 return Ok(staged_commit);
448 }
449
450 #[cfg(feature = "virtual-clients-draft")]
459 let vc_path: Option<(Vec<EncryptionKeyPair>, CommitSecret)> = if sender_index
460 == self.own_leaf_index()
461 || is_sibling_resync
462 {
463 let operation_secret = vc_material.ok_or(
464 crate::components::vc_derivation_info::VirtualClientsError::MissingOperationTree,
465 )?;
466 Some(self.recreate_path_for_own_commit(
467 &diff,
468 &path,
469 ciphersuite,
470 self.group_id(),
471 provider.crypto(),
472 sender_index,
473 operation_secret,
474 )?)
475 } else {
476 None
477 };
478 #[cfg(not(feature = "virtual-clients-draft"))]
479 let vc_path: Option<(Vec<EncryptionKeyPair>, CommitSecret)> = None;
480
481 let (new_keypairs, commit_secret) = if let Some(pair) = vc_path {
484 pair
485 } else {
486 let decryption_keypairs: Vec<&EncryptionKeyPair> = old_epoch_keypairs
487 .iter()
488 .chain(leaf_node_keypairs.iter())
489 .collect();
490 diff.decrypt_path(
491 provider.crypto(),
492 &decryption_keypairs,
493 self.own_leaf_index(),
494 sender_index,
495 path.nodes(),
496 &apply_proposals_values.exclusion_list(),
497 )?
498 };
499
500 let new_leaf_keypair_option = if is_sibling_resync {
506 None
507 } else if let Some(leaf) = diff.leaf(self.own_leaf_index()) {
508 leaf_node_keypairs.into_iter().find_map(|keypair| {
509 if leaf.encryption_key() == keypair.public_key() {
510 Some(keypair)
511 } else {
512 None
513 }
514 })
515 } else {
516 debug_assert!(false);
518 None
519 };
520
521 let update_path_leaf_node = Some(path.leaf_node().clone());
525 debug_assert_eq!(diff.leaf(sender_index), path.leaf_node().into());
526
527 (
528 commit_secret,
529 new_keypairs,
530 new_leaf_keypair_option,
531 update_path_leaf_node,
532 )
533 } else {
534 if apply_proposals_values.path_required {
535 return Err(StageCommitError::RequiredPathNotFound);
537 }
538
539 diff.update_group_context(
541 provider.crypto(),
542 apply_proposals_values.extensions.clone(),
543 )?;
544
545 (CommitSecret::zero_secret(ciphersuite), vec![], None, None)
546 };
547
548 diff.update_confirmed_transcript_hash(provider.crypto(), mls_content)?;
550
551 let received_confirmation_tag = mls_content
552 .confirmation_tag()
553 .ok_or(StageCommitError::ConfirmationTagMissing)?;
554
555 let serialized_provisional_group_context = diff
556 .group_context()
557 .tls_serialize_detached()
558 .map_err(LibraryError::missing_bound_check)?;
559
560 #[cfg(feature = "virtual-clients-draft")]
561 let provisional_own_leaf_index = if is_sibling_resync {
562 sender_index
563 } else {
564 self.own_leaf_index()
565 };
566 #[cfg(not(feature = "virtual-clients-draft"))]
567 let provisional_own_leaf_index = self.own_leaf_index();
568
569 let EpochSecretsResult {
570 epoch_secrets,
571 #[cfg(feature = "extensions-draft")]
572 application_exporter,
573 } = self.derive_epoch_secrets(
574 provider,
575 apply_proposals_values,
576 self.group_epoch_secrets(),
577 commit_secret,
578 &serialized_provisional_group_context,
579 #[cfg(feature = "virtual-clients-draft")]
580 vc_external_init_secret.as_ref(),
581 )?;
582 let (provisional_group_secrets, provisional_message_secrets) = epoch_secrets.split_secrets(
583 serialized_provisional_group_context,
584 diff.tree_size(),
585 provisional_own_leaf_index,
586 );
587
588 let own_confirmation_tag = provisional_message_secrets
591 .confirmation_key()
592 .tag(
593 provider.crypto(),
594 self.ciphersuite(),
595 diff.group_context().confirmed_transcript_hash(),
596 )
597 .map_err(LibraryError::unexpected_crypto_error)?;
598 if &own_confirmation_tag != received_confirmation_tag {
599 log::error!("Confirmation tag mismatch");
600 log_crypto!(trace, " Got: {:x?}", received_confirmation_tag);
601 log_crypto!(trace, " Expected: {:x?}", own_confirmation_tag);
602 if !crate::skip_validation::is_disabled::confirmation_tag() {
609 return Err(StageCommitError::ConfirmationTagMismatch);
610 }
611 }
612
613 diff.update_interim_transcript_hash(ciphersuite, provider.crypto(), own_confirmation_tag)?;
614
615 let staged_diff = diff.into_staged_diff(provider.crypto(), ciphersuite)?;
616 #[cfg(feature = "extensions-draft")]
617 let application_export_tree = ApplicationExportTree::new(application_exporter);
618 #[cfg(feature = "virtual-clients-draft")]
619 let new_own_leaf_index = is_sibling_resync.then_some(provisional_own_leaf_index);
620 let staged_commit_state =
621 StagedCommitState::GroupMember(Box::new(MemberStagedCommitState::new(
622 provisional_group_secrets,
623 provisional_message_secrets,
624 staged_diff,
625 new_keypairs,
626 new_leaf_keypair_option,
627 update_path_leaf_node,
628 #[cfg(feature = "extensions-draft")]
629 application_export_tree,
630 #[cfg(feature = "virtual-clients-draft")]
631 new_own_leaf_index,
632 )));
633 #[cfg_attr(not(feature = "virtual-clients-draft"), allow(unused_mut))]
634 let mut staged_commit = StagedCommit::new(
635 proposal_queue,
636 staged_commit_state,
637 #[cfg(feature = "virtual-clients-draft")]
638 vc_derivation_epoch_id,
639 );
640 #[cfg(feature = "virtual-clients-draft")]
641 {
642 staged_commit.marks_new_vc_derivation_epoch = marks_new_vc_derivation_epoch;
643 }
644
645 Ok(staged_commit)
646 }
647
648 #[cfg(feature = "virtual-clients-draft")]
670 #[expect(clippy::too_many_arguments)]
671 fn recreate_path_for_own_commit(
672 &self,
673 diff: &PublicGroupDiff,
674 path: &crate::treesync::treekem::UpdatePath,
675 group_ciphersuite: openmls_traits::types::Ciphersuite,
676 group_id: &crate::prelude::GroupId,
677 crypto: &impl OpenMlsCrypto,
678 sender_index: LeafNodeIndex,
679 operation_secret: crate::components::vc_derivation_info::OperationSecret,
680 ) -> Result<(Vec<EncryptionKeyPair>, CommitSecret), StageCommitError> {
681 use crate::components::vc_derivation_info::VirtualClientsError;
682
683 let target_operation_secret =
684 operation_secret.derive_target_operation_secret(crypto, group_ciphersuite, group_id)?;
685
686 let path_secret = target_operation_secret
687 .derive_path_generation_secret(crypto, group_ciphersuite)?
688 .into();
689 let (encryption_key_pairs, commit_secret) =
690 diff.recreate_path_from_path_secret(crypto, path_secret, sender_index, path.nodes())?;
691
692 let leaf_keypair = target_operation_secret
695 .derive_encryption_key_secret(crypto, group_ciphersuite)?
696 .generate_encryption_key_pair(crypto, group_ciphersuite)?;
697 drop(target_operation_secret);
698 if leaf_keypair.public_key() != path.leaf_node().encryption_key() {
699 return Err(VirtualClientsError::EncryptionKeyMismatch.into());
700 }
701
702 let mut keypairs = Vec::with_capacity(1 + encryption_key_pairs.len());
706 keypairs.push(leaf_keypair);
707 keypairs.extend(encryption_key_pairs);
708 Ok((keypairs, commit_secret))
709 }
710
711 #[cfg(feature = "virtual-clients-draft")]
723 pub(crate) fn commit_creates_vc_derivation_epoch(&self, staged_commit: &StagedCommit) -> bool {
724 self.is_emulation_group()
725 && matches!(staged_commit.state, StagedCommitState::GroupMember(_))
726 && (staged_commit.marks_new_vc_derivation_epoch || staged_commit.changes_membership())
727 }
728
729 pub(crate) fn merge_commit<Provider: OpenMlsProvider>(
738 &mut self,
739 provider: &Provider,
740 staged_commit: StagedCommit,
741 ) -> Result<(), MergeCommitError<Provider::StorageError>> {
742 let old_epoch_keypairs = self
745 .read_epoch_keypairs(provider.storage())
746 .map_err(MergeCommitError::StorageError)?;
747
748 #[cfg(feature = "virtual-clients-draft")]
749 let creates_vc_derivation_epoch = self.commit_creates_vc_derivation_epoch(&staged_commit);
750
751 match staged_commit.state {
752 StagedCommitState::PublicState(staged_state) => {
753 self.public_group
754 .merge_diff(staged_state.into_staged_diff());
755 self.store(provider.storage())
756 .map_err(MergeCommitError::StorageError)?;
757 Ok(())
758 }
759 StagedCommitState::GroupMember(state) => {
760 let past_epoch = self.context().epoch();
762 let leaves = self.public_group().members().collect();
764 self.group_epoch_secrets = state.group_epoch_secrets;
767
768 let old_message_secrets = self
770 .message_secrets_store
771 .replace_current_message_secrets(state.message_secrets);
772 self.message_secrets_store.add_past_epoch_tree(
773 past_epoch,
774 old_message_secrets,
775 leaves,
776 );
777
778 self.public_group.merge_diff(state.staged_diff);
779
780 #[cfg(feature = "virtual-clients-draft")]
781 let previous_own_leaf_index = self.own_leaf_index;
782
783 #[cfg(feature = "virtual-clients-draft")]
788 if let Some(new_idx) = state.new_own_leaf_index {
789 self.own_leaf_index = new_idx;
790 }
791
792 #[cfg(feature = "extensions-draft")]
797 {
798 #[cfg_attr(not(feature = "virtual-clients-draft"), allow(unused_mut))]
804 let mut application_export_tree = state.application_export_tree;
805
806 #[cfg(feature = "virtual-clients-draft")]
809 if creates_vc_derivation_epoch {
810 crate::components::vc_derivation_info::register_vc_derivation_epoch(
811 provider.crypto(),
812 provider.storage(),
813 application_export_tree.as_mut(),
814 crate::components::vc_derivation_info::VcDerivationEpochParams::for_public_group(
815 self.public_group(),
816 self.own_leaf_index(),
817 self.mls_group_config
818 .vc_derivation_epoch_retention_policy()
819 .clone(),
820 ),
821 )?;
822 }
823
824 if let Some(application_export_tree) = application_export_tree {
825 use openmls_traits::storage::StorageProvider as _;
827 provider
828 .storage()
829 .write_application_export_tree(
830 self.group_id(),
831 &application_export_tree,
832 )
833 .map_err(MergeCommitError::StorageError)?;
834
835 self.application_export_tree = Some(application_export_tree);
836 }
837 }
838
839 let leaf_keypair = if let Some(keypair) = &state.new_leaf_keypair_option {
840 vec![keypair.clone()]
841 } else {
842 vec![]
843 };
844
845 let new_owned_encryption_keys = self
847 .public_group()
848 .owned_encryption_keys(self.own_leaf_index());
849 let epoch_keypairs: Vec<EncryptionKeyPair> = old_epoch_keypairs
851 .into_iter()
852 .chain(state.new_keypairs)
853 .chain(leaf_keypair)
854 .filter(|keypair| new_owned_encryption_keys.contains(keypair.public_key()))
855 .collect();
856
857 debug_assert_eq!(new_owned_encryption_keys.len(), epoch_keypairs.len());
859 if new_owned_encryption_keys.len() != epoch_keypairs.len() {
860 return Err(LibraryError::custom(
861 "We should have all the private key material we need.",
862 )
863 .into());
864 }
865
866 let storage = provider.storage();
868 let group_id = self.group_id();
869
870 self.public_group
871 .store(storage)
872 .map_err(MergeCommitError::StorageError)?;
873 storage
874 .write_own_leaf_index(group_id, &self.own_leaf_index)
875 .map_err(MergeCommitError::StorageError)?;
876 storage
877 .write_group_epoch_secrets(group_id, &self.group_epoch_secrets)
878 .map_err(MergeCommitError::StorageError)?;
879 storage
880 .write_message_secrets(group_id, &self.message_secrets_store)
881 .map_err(MergeCommitError::StorageError)?;
882
883 self.store_epoch_keypairs(storage, epoch_keypairs.as_slice())
885 .map_err(MergeCommitError::StorageError)?;
886
887 self.delete_previous_epoch_keypairs(
889 storage,
890 #[cfg(feature = "virtual-clients-draft")]
891 previous_own_leaf_index,
892 )
893 .map_err(MergeCommitError::StorageError)?;
894 if let Some(keypair) = state.new_leaf_keypair_option {
895 keypair
896 .delete(storage)
897 .map_err(MergeCommitError::StorageError)?;
898 }
899
900 storage
902 .clear_proposal_queue::<GroupId, ProposalRef>(group_id)
903 .map_err(MergeCommitError::StorageError)?;
904 self.proposal_store_mut().empty();
905
906 Ok(())
907 }
908 }
909 }
910}
911
912#[derive(Debug, Serialize, Deserialize)]
913#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
914pub(crate) enum StagedCommitState {
915 PublicState(Box<PublicStagedCommitState>),
916 GroupMember(Box<MemberStagedCommitState>),
918}
919
920#[derive(Debug, Serialize, Deserialize)]
922#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
923pub struct StagedCommit {
924 pub staged_proposal_queue: ProposalQueue,
926 pub(super) state: StagedCommitState,
928 #[cfg(feature = "virtual-clients-draft")]
931 #[serde(default, alias = "vc_emulation_epoch_id")]
932 pub(super) vc_derivation_epoch_id: Option<crate::components::vc_derivation_info::EpochId>,
934 #[cfg(feature = "virtual-clients-draft")]
940 #[serde(default)]
941 pub(super) marks_new_vc_derivation_epoch: bool,
942}
943
944impl StagedCommit {
945 pub(crate) fn new(
948 staged_proposal_queue: ProposalQueue,
949 state: StagedCommitState,
950 #[cfg(feature = "virtual-clients-draft")] vc_derivation_epoch_id: Option<
951 crate::components::vc_derivation_info::EpochId,
952 >,
953 ) -> Self {
954 StagedCommit {
955 staged_proposal_queue,
956 state,
957 #[cfg(feature = "virtual-clients-draft")]
958 vc_derivation_epoch_id,
959 #[cfg(feature = "virtual-clients-draft")]
960 marks_new_vc_derivation_epoch: false,
961 }
962 }
963
964 #[cfg(feature = "virtual-clients-draft")]
967 fn changes_membership(&self) -> bool {
968 self.staged_proposal_queue
969 .queued_proposals()
970 .any(|queued| match queued.proposal() {
971 Proposal::Add(_)
972 | Proposal::Remove(_)
973 | Proposal::SelfRemove
974 | Proposal::ExternalInit(_) => true,
975 Proposal::Update(_)
976 | Proposal::PreSharedKey(_)
977 | Proposal::ReInit(_)
978 | Proposal::GroupContextExtensions(_)
979 | Proposal::AppDataUpdate(_)
980 | Proposal::AppEphemeral(_)
981 | Proposal::Custom(_) => false,
982 })
983 }
984
985 pub fn epoch(&self) -> GroupEpoch {
987 self.group_context().epoch()
988 }
989
990 pub fn export_ratchet_tree(
992 &self,
993 crypto: &impl OpenMlsCrypto,
994 original_tree: RatchetTree,
995 ) -> Result<Option<RatchetTree>, TreeSyncFromNodesError> {
996 match &self.state {
997 StagedCommitState::PublicState(_public_staged_commit_state) => Ok(None),
998 StagedCommitState::GroupMember(member_staged_commit_state) => Ok(Some(
999 member_staged_commit_state.staged_diff.export_ratchet_tree(
1000 crypto,
1001 self.group_context().ciphersuite(),
1002 original_tree,
1003 )?,
1004 )),
1005 }
1006 }
1007
1008 pub fn add_proposals(&self) -> impl Iterator<Item = QueuedAddProposal<'_>> {
1010 self.staged_proposal_queue.add_proposals()
1011 }
1012
1013 pub fn remove_proposals(&self) -> impl Iterator<Item = QueuedRemoveProposal<'_>> {
1015 self.staged_proposal_queue.remove_proposals()
1016 }
1017
1018 pub fn update_proposals(&self) -> impl Iterator<Item = QueuedUpdateProposal<'_>> {
1020 self.staged_proposal_queue.update_proposals()
1021 }
1022
1023 pub fn psk_proposals(&self) -> impl Iterator<Item = QueuedPskProposal<'_>> {
1025 self.staged_proposal_queue.psk_proposals()
1026 }
1027
1028 pub fn reinit_proposal(&self) -> Option<&ReInitProposal> {
1034 self.staged_proposal_queue.reinit_proposal()
1035 }
1036
1037 #[cfg(feature = "extensions-draft")]
1038 pub fn queued_app_ephemeral_proposals(
1041 &self,
1042 ) -> impl Iterator<Item = QueuedAppEphemeralProposal<'_>> {
1043 self.staged_proposal_queue.app_ephemeral_proposals()
1044 }
1045 #[cfg(feature = "extensions-draft")]
1047 pub fn app_data_update_proposals(
1050 &self,
1051 ) -> impl Iterator<Item = QueuedAppDataUpdateProposal<'_>> {
1052 self.staged_proposal_queue.app_data_update_proposals()
1053 }
1054
1055 pub fn queued_proposals(&self) -> impl Iterator<Item = &QueuedProposal> {
1057 self.staged_proposal_queue.queued_proposals()
1058 }
1059
1060 pub fn update_path_leaf_node(&self) -> Option<&LeafNode> {
1062 match self.state {
1063 StagedCommitState::PublicState(ref public_state) => {
1064 public_state.update_path_leaf_node()
1065 }
1066 StagedCommitState::GroupMember(ref group_member_state) => {
1067 group_member_state.update_path_leaf_node.as_ref()
1068 }
1069 }
1070 }
1071
1072 pub fn credentials_to_verify(&self) -> impl Iterator<Item = &Credential> {
1074 let update_path_leaf_node_cred = if let Some(node) = self.update_path_leaf_node() {
1075 vec![node.credential()]
1076 } else {
1077 vec![]
1078 };
1079
1080 update_path_leaf_node_cred
1081 .into_iter()
1082 .chain(
1083 self.queued_proposals()
1084 .flat_map(|proposal: &QueuedProposal| match proposal.proposal() {
1085 Proposal::Update(update_proposal) => {
1086 vec![update_proposal.leaf_node().credential()].into_iter()
1087 }
1088 Proposal::Add(add_proposal) => {
1089 vec![add_proposal.key_package().leaf_node().credential()].into_iter()
1090 }
1091 Proposal::GroupContextExtensions(gce_proposal) => gce_proposal
1092 .extensions()
1093 .iter()
1094 .flat_map(|extension| {
1095 match extension {
1096 Extension::ExternalSenders(external_senders) => {
1097 external_senders
1098 .iter()
1099 .map(|external_sender| external_sender.credential())
1100 .collect()
1101 }
1102 _ => vec![],
1103 }
1104 .into_iter()
1105 })
1106 .collect::<Vec<_>>()
1113 .into_iter(),
1114 _ => vec![].into_iter(),
1115 }),
1116 )
1117 }
1118
1119 pub fn self_removed(&self) -> bool {
1128 matches!(self.state, StagedCommitState::PublicState(_))
1129 }
1130
1131 pub fn group_context(&self) -> &GroupContext {
1133 match self.state {
1134 StagedCommitState::PublicState(ref ps) => ps.staged_diff().group_context(),
1135 StagedCommitState::GroupMember(ref gm) => gm.group_context(),
1136 }
1137 }
1138 pub(crate) fn into_state(self) -> StagedCommitState {
1140 self.state
1141 }
1142
1143 pub fn epoch_authenticator(&self) -> Option<&EpochAuthenticator> {
1147 if let StagedCommitState::GroupMember(ref gm) = self.state {
1148 Some(gm.group_epoch_secrets.epoch_authenticator())
1149 } else {
1150 None
1151 }
1152 }
1153
1154 pub fn resumption_psk_secret(&self) -> Option<&ResumptionPskSecret> {
1158 if let StagedCommitState::GroupMember(ref gm) = self.state {
1159 Some(gm.group_epoch_secrets.resumption_psk())
1160 } else {
1161 None
1162 }
1163 }
1164
1165 #[cfg(feature = "extensions-draft")]
1173 pub fn safe_export_secret(
1174 &mut self,
1175 crypto: &impl OpenMlsCrypto,
1176 component_id: ComponentId,
1177 ) -> Result<ExportedSecret<StagedCommitSafeExport>, StagedSafeExportSecretError> {
1178 let ciphersuite = self.group_context().ciphersuite();
1179 let StagedCommitState::GroupMember(ref mut staged_commit) = self.state else {
1180 return Err(StagedSafeExportSecretError::NotGroupMember);
1181 };
1182 let Some(application_export_tree) = staged_commit.application_export_tree.as_mut() else {
1183 return Err(StagedSafeExportSecretError::Unsupported);
1184 };
1185 let secret =
1186 application_export_tree.safe_export_secret(crypto, ciphersuite, component_id)?;
1187 Ok(ExportedSecret::new(secret))
1188 }
1189
1190 pub fn export_secret<CryptoProvider: OpenMlsCrypto>(
1198 &self,
1199 crypto: &CryptoProvider,
1200 label: &str,
1201 context: &[u8],
1202 key_length: usize,
1203 ) -> Result<ExportedSecret<StagedCommitExport>, ExportSecretError> {
1204 if key_length > u16::MAX as usize {
1205 log::error!("Got a key that is larger than u16::MAX");
1206 return Err(ExportSecretError::KeyLengthTooLong);
1207 }
1208
1209 match &self.state {
1210 StagedCommitState::PublicState(_public_staged_commit_state) => Err(
1211 ExportSecretError::GroupStateError(MlsGroupStateError::UseAfterEviction),
1212 ),
1213 StagedCommitState::GroupMember(member_staged_commit_state) => Ok(ExportedSecret::new(
1214 member_staged_commit_state
1215 .group_epoch_secrets
1216 .exporter_secret()
1217 .derive_exported_secret(
1218 self.group_context().ciphersuite(),
1219 crypto,
1220 label,
1221 context,
1222 key_length,
1223 )
1224 .map_err(LibraryError::unexpected_crypto_error)?,
1225 )),
1226 }
1227 }
1228}
1229
1230#[derive(Debug, Serialize, Deserialize)]
1232#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
1233pub(crate) struct MemberStagedCommitState {
1234 group_epoch_secrets: GroupEpochSecrets,
1235 message_secrets: MessageSecrets,
1236 staged_diff: StagedPublicGroupDiff,
1237 new_keypairs: Vec<EncryptionKeyPair>,
1238 new_leaf_keypair_option: Option<EncryptionKeyPair>,
1239 update_path_leaf_node: Option<LeafNode>,
1240 #[cfg(feature = "extensions-draft")]
1241 #[serde(default)]
1242 application_export_tree: Option<ApplicationExportTree>,
1245 #[cfg(feature = "virtual-clients-draft")]
1250 #[serde(default)]
1251 new_own_leaf_index: Option<LeafNodeIndex>,
1252}
1253
1254impl MemberStagedCommitState {
1255 #[allow(clippy::too_many_arguments)]
1256 pub(crate) fn new(
1257 group_epoch_secrets: GroupEpochSecrets,
1258 message_secrets: MessageSecrets,
1259 staged_diff: StagedPublicGroupDiff,
1260 new_keypairs: Vec<EncryptionKeyPair>,
1261 new_leaf_keypair_option: Option<EncryptionKeyPair>,
1262 update_path_leaf_node: Option<LeafNode>,
1263 #[cfg(feature = "extensions-draft")] application_export_tree: ApplicationExportTree,
1264 #[cfg(feature = "virtual-clients-draft")] new_own_leaf_index: Option<LeafNodeIndex>,
1265 ) -> Self {
1266 Self {
1267 group_epoch_secrets,
1268 message_secrets,
1269 staged_diff,
1270 new_keypairs,
1271 new_leaf_keypair_option,
1272 update_path_leaf_node,
1273 #[cfg(feature = "extensions-draft")]
1274 application_export_tree: Some(application_export_tree),
1275 #[cfg(feature = "virtual-clients-draft")]
1276 new_own_leaf_index,
1277 }
1278 }
1279
1280 pub(crate) fn group_context(&self) -> &GroupContext {
1282 self.staged_diff.group_context()
1283 }
1284}
1285
1286#[cfg(all(test, feature = "virtual-clients-draft"))]
1287mod tests {
1288 use super::validate_vc_external_init_secret;
1289 use crate::{
1290 components::vc_derivation_info::VirtualClientsError, group::errors::StageCommitError,
1291 };
1292
1293 #[test]
1298 fn external_init_secret_presence_is_validated() {
1299 let malformed: Result<(), StageCommitError> =
1300 Err(VirtualClientsError::DerivationInfoMalformed.into());
1301
1302 assert_eq!(
1304 validate_vc_external_init_secret(true, true, false),
1305 malformed
1306 );
1307 assert_eq!(
1309 validate_vc_external_init_secret(false, false, true),
1310 malformed
1311 );
1312 assert_eq!(
1313 validate_vc_external_init_secret(true, false, true),
1314 malformed
1315 );
1316
1317 assert_eq!(validate_vc_external_init_secret(true, true, true), Ok(()));
1320 assert_eq!(validate_vc_external_init_secret(false, true, false), Ok(()));
1321 assert_eq!(
1322 validate_vc_external_init_secret(false, false, false),
1323 Ok(())
1324 );
1325 }
1326}