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::*, Credential, ExportedSecret, Extension, GroupContext, GroupEpochSecrets,
18 GroupId, JoinerSecret, KeySchedule, LeafNode, LibraryError, MessageSecrets, MlsGroup,
19 MlsGroupState, OpenMlsProvider, PendingCommitState, Proposal, ProposalQueue, PskSecret,
20 QueuedProposal, ReInitProposal, StagedCommitExport,
21};
22use crate::group::diff::PublicGroupDiff;
23use crate::group::{GroupEpoch, LeafNodeLifetimePolicy};
24use crate::messages::ConfirmationTag;
25use crate::prelude::{Commit, LeafNodeIndex};
26use crate::schedule::psk::load_psks;
27#[cfg(feature = "extensions-draft")]
28use crate::{component::ComponentId, schedule::application_export_tree::ApplicationExportTree};
29
30use crate::treesync::errors::TreeSyncFromNodesError;
31use crate::treesync::RatchetTree;
32use crate::{
33 ciphersuite::{hash_ref::ProposalRef, Secret},
34 framing::mls_auth_content::AuthenticatedContent,
35 group::public_group::{
36 diff::{apply_proposals::ApplyProposalsValues, StagedPublicGroupDiff},
37 staged_commit::PublicStagedCommitState,
38 },
39 schedule::{
40 CommitSecret, EpochAuthenticator, EpochSecretsResult, InitSecret, PreSharedKeyId,
41 ResumptionPskSecret,
42 },
43 treesync::node::encryption_keys::EncryptionKeyPair,
44};
45
46#[cfg(feature = "extensions-draft")]
47use super::proposal_store::{QueuedAppDataUpdateProposal, QueuedAppEphemeralProposal};
48#[cfg(feature = "extensions-draft")]
49use crate::prelude::processing::AppDataUpdates;
50
51#[cfg(feature = "virtual-clients-draft")]
52fn validate_vc_external_init_secret(
53 is_sibling_resync: bool,
54 has_external_init_proposal: bool,
55 has_vc_external_init_secret: bool,
56) -> Result<(), StageCommitError> {
57 if is_sibling_resync && !has_vc_external_init_secret {
58 return Err(
59 crate::components::vc_derivation_info::VirtualClientsError::DerivationInfoMalformed
60 .into(),
61 );
62 }
63 if has_vc_external_init_secret && !has_external_init_proposal {
64 return Err(
65 crate::components::vc_derivation_info::VirtualClientsError::DerivationInfoMalformed
66 .into(),
67 );
68 }
69 Ok(())
70}
71
72#[cfg(feature = "virtual-clients-draft")]
80fn commit_marks_new_vc_derivation_epoch(
81 authenticated_data: &[u8],
82) -> Result<bool, StageCommitError> {
83 use crate::components::vc_commit_data::VirtualClientCommitData;
84
85 let (safe_aad, _prefix_len) =
86 crate::framing::safe_aad::parse_authenticated_data_prefix(authenticated_data)
87 .map_err(|e| StageCommitError::MalformedVcCommitData(e.to_string()))?;
88 let commit_data = VirtualClientCommitData::from_safe_aad(&safe_aad)
89 .map_err(|e| StageCommitError::MalformedVcCommitData(e.to_string()))?;
90 Ok(commit_data.is_some_and(|commit_data| commit_data.creates_derivation_epoch()))
91}
92
93impl MlsGroup {
94 pub(crate) fn matches_pending_commit(&self, received_tag: &ConfirmationTag) -> bool {
103 let MlsGroupState::PendingCommit(pending_commit_state) = &self.group_state else {
104 return false;
105 };
106 let PendingCommitState::Member(staged_commit) = pending_commit_state.as_ref() else {
107 return false;
108 };
109 let StagedCommitState::GroupMember(member_state) = &staged_commit.state else {
110 return false;
111 };
112 member_state.staged_diff.confirmation_tag() == received_tag
113 }
114
115 #[openmls_traits::maybe_async]
116 async fn derive_epoch_secrets(
117 &self,
118 provider: &impl OpenMlsProvider,
119 apply_proposals_values: ApplyProposalsValues,
120 epoch_secrets: &GroupEpochSecrets,
121 commit_secret: CommitSecret,
122 serialized_provisional_group_context: &[u8],
123 #[cfg(feature = "virtual-clients-draft")] vc_external_init_secret: Option<
124 &crate::components::vc_derivation_info::ExternalInitSecret,
125 >,
126 ) -> Result<EpochSecretsResult, StageCommitError> {
127 let joiner_secret = if let Some(ref external_init_proposal) =
130 apply_proposals_values.external_init_proposal_option
131 {
132 #[cfg(feature = "virtual-clients-draft")]
139 let carried_init_secret = vc_external_init_secret
140 .map(|carried| InitSecret::from(Secret::from_slice(carried.as_slice())));
141 #[cfg(not(feature = "virtual-clients-draft"))]
142 let carried_init_secret: Option<InitSecret> = None;
143
144 let init_secret = match carried_init_secret {
145 Some(init_secret) => init_secret,
146 None => {
147 let external_priv = epoch_secrets
148 .external_secret()
149 .derive_external_keypair(provider.crypto(), self.ciphersuite())
150 .map_err(LibraryError::unexpected_crypto_error)?
151 .private;
152 InitSecret::from_kem_output(
153 provider.crypto(),
154 self.ciphersuite(),
155 self.version(),
156 &external_priv,
157 external_init_proposal.kem_output(),
158 )?
159 }
160 };
161 JoinerSecret::new(
162 provider.crypto(),
163 self.ciphersuite(),
164 commit_secret,
165 &init_secret,
166 serialized_provisional_group_context,
167 )
168 .map_err(LibraryError::unexpected_crypto_error)?
169 } else {
170 JoinerSecret::new(
171 provider.crypto(),
172 self.ciphersuite(),
173 commit_secret,
174 epoch_secrets.init_secret(),
175 serialized_provisional_group_context,
176 )
177 .map_err(LibraryError::unexpected_crypto_error)?
178 };
179
180 let psk_secret = {
183 let psks: Vec<(&PreSharedKeyId, Secret)> = load_psks(
184 provider.storage(),
185 &self.resumption_psk_store,
186 Some(self.group_id()),
187 None,
188 &apply_proposals_values.presharedkeys,
189 )
190 .await?;
191
192 PskSecret::new(provider.crypto(), self.ciphersuite(), psks)?
193 };
194
195 let mut key_schedule = KeySchedule::init(
197 self.ciphersuite(),
198 provider.crypto(),
199 &joiner_secret,
200 psk_secret,
201 )?;
202
203 key_schedule
204 .add_context(provider.crypto(), serialized_provisional_group_context)
205 .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
206 Ok(key_schedule
207 .epoch_secrets(provider.crypto(), self.ciphersuite())
208 .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?)
209 }
210
211 #[openmls_traits::maybe_async]
250 pub(crate) async fn stage_commit(
251 &self,
252 mls_content: &AuthenticatedContent,
253 old_epoch_keypairs: Vec<EncryptionKeyPair>,
254 leaf_node_keypairs: Vec<EncryptionKeyPair>,
255 provider: &impl OpenMlsProvider,
256 lifetime_policy: LeafNodeLifetimePolicy,
257 #[cfg(feature = "virtual-clients-draft")] vc_commit_material: Option<
258 crate::components::vc_derivation_info::VcCommitMaterial,
259 >,
260 ) -> Result<StagedCommit, StageCommitError> {
261 let (commit, proposal_queue, sender_index) =
262 self.public_group
263 .validate_commit(mls_content, provider.crypto(), lifetime_policy)?;
264
265 let mut diff = self.public_group.empty_diff();
268
269 #[cfg(not(feature = "extensions-draft"))]
270 let apply_proposals_values =
271 diff.apply_proposals(&proposal_queue, self.own_leaf_index())?;
272
273 #[cfg(feature = "extensions-draft")]
274 let apply_proposals_values = diff.apply_proposals_with_app_data_updates(
275 &proposal_queue,
276 self.own_leaf_index(),
277 None,
278 )?;
279 self.stage_applied_proposal_values(
280 apply_proposals_values,
281 diff,
282 commit,
283 proposal_queue,
284 sender_index,
285 mls_content,
286 old_epoch_keypairs,
287 leaf_node_keypairs,
288 provider,
289 #[cfg(feature = "virtual-clients-draft")]
290 vc_commit_material,
291 )
292 .await
293 }
294
295 #[cfg(feature = "extensions-draft")]
296 #[allow(clippy::too_many_arguments)]
297 #[openmls_traits::maybe_async]
298 pub(crate) async fn stage_commit_with_app_data_updates(
299 &self,
300 mls_content: &AuthenticatedContent,
301 old_epoch_keypairs: Vec<EncryptionKeyPair>,
302 leaf_node_keypairs: Vec<EncryptionKeyPair>,
303 app_data_dict_updates: Option<AppDataUpdates>,
304 provider: &impl OpenMlsProvider,
305 lifetime_policy: LeafNodeLifetimePolicy,
306 #[cfg(feature = "virtual-clients-draft")] vc_commit_material: Option<
307 crate::components::vc_derivation_info::VcCommitMaterial,
308 >,
309 ) -> Result<StagedCommit, StageCommitError> {
310 let (commit, proposal_queue, sender_index) =
311 self.public_group
312 .validate_commit(mls_content, provider.crypto(), lifetime_policy)?;
313
314 let mut diff = self.public_group.empty_diff();
317
318 let apply_proposals_values = diff.apply_proposals_with_app_data_updates(
319 &proposal_queue,
320 self.own_leaf_index(),
321 app_data_dict_updates,
322 )?;
323
324 self.stage_applied_proposal_values(
325 apply_proposals_values,
326 diff,
327 commit,
328 proposal_queue,
329 sender_index,
330 mls_content,
331 old_epoch_keypairs,
332 leaf_node_keypairs,
333 provider,
334 #[cfg(feature = "virtual-clients-draft")]
335 vc_commit_material,
336 )
337 .await
338 }
339
340 #[allow(clippy::too_many_arguments)]
341 #[openmls_traits::maybe_async]
342 async fn stage_applied_proposal_values(
343 &self,
344 apply_proposals_values: ApplyProposalsValues,
345 mut diff: PublicGroupDiff<'_>,
346 commit: &Commit,
347 proposal_queue: ProposalQueue,
348 sender_index: LeafNodeIndex,
349 mls_content: &AuthenticatedContent,
350 old_epoch_keypairs: Vec<EncryptionKeyPair>,
351 leaf_node_keypairs: Vec<EncryptionKeyPair>,
352 provider: &impl OpenMlsProvider,
353 #[cfg(feature = "virtual-clients-draft")] vc_commit_material: Option<
354 crate::components::vc_derivation_info::VcCommitMaterial,
355 >,
356 ) -> Result<StagedCommit, StageCommitError> {
357 let ciphersuite = self.ciphersuite();
358
359 #[cfg(feature = "virtual-clients-draft")]
362 let marks_new_vc_derivation_epoch =
363 if self.is_emulation_group() && self.context().safe_aad_required() {
364 commit_marks_new_vc_derivation_epoch(mls_content.authenticated_data())?
365 } else {
366 false
367 };
368
369 #[cfg(feature = "virtual-clients-draft")]
374 let (vc_material, vc_derivation_epoch_id, vc_external_init_secret) =
375 match vc_commit_material {
376 Some(material) => (
377 Some(material.operation_secret),
378 Some(material.epoch_id),
379 material.external_init_secret,
380 ),
381 None => (None, None, None),
382 };
383
384 #[cfg(feature = "virtual-clients-draft")]
405 let is_sibling_resync =
406 vc_material.is_some() && matches!(mls_content.sender(), Sender::NewMemberCommit);
407 #[cfg(not(feature = "virtual-clients-draft"))]
408 let is_sibling_resync = false;
409
410 #[cfg(feature = "virtual-clients-draft")]
415 validate_vc_external_init_secret(
416 is_sibling_resync,
417 apply_proposals_values
418 .external_init_proposal_option
419 .is_some(),
420 vc_external_init_secret.is_some(),
421 )?;
422
423 let (commit_secret, new_keypairs, new_leaf_keypair_option, update_path_leaf_node) =
425 if let Some(path) = commit.path.clone() {
426 diff.apply_received_update_path(
429 provider.crypto(),
430 ciphersuite,
431 sender_index,
432 &path,
433 )?;
434
435 diff.update_group_context(
437 provider.crypto(),
438 apply_proposals_values.extensions.clone(),
439 )?;
440
441 if apply_proposals_values.self_removed && !is_sibling_resync {
447 let staged_diff = diff.into_staged_diff(provider.crypto(), ciphersuite)?;
449 let staged_state = PublicStagedCommitState::new(
450 staged_diff,
451 commit.path.as_ref().map(|path| path.leaf_node().clone()),
452 );
453 let staged_commit = StagedCommit::new(
454 proposal_queue,
455 StagedCommitState::PublicState(Box::new(staged_state)),
456 #[cfg(feature = "virtual-clients-draft")]
457 None,
458 );
459 return Ok(staged_commit);
460 }
461
462 #[cfg(feature = "virtual-clients-draft")]
471 let vc_path: Option<(Vec<EncryptionKeyPair>, CommitSecret)> = if sender_index
472 == self.own_leaf_index()
473 || is_sibling_resync
474 {
475 let operation_secret = vc_material.ok_or(
476 crate::components::vc_derivation_info::VirtualClientsError::MissingOperationTree,
477 )?;
478 Some(self.recreate_path_for_own_commit(
479 &diff,
480 &path,
481 ciphersuite,
482 self.group_id(),
483 provider.crypto(),
484 sender_index,
485 operation_secret,
486 )?)
487 } else {
488 None
489 };
490 #[cfg(not(feature = "virtual-clients-draft"))]
491 let vc_path: Option<(Vec<EncryptionKeyPair>, CommitSecret)> = None;
492
493 let (new_keypairs, commit_secret) = if let Some(pair) = vc_path {
496 pair
497 } else {
498 let decryption_keypairs: Vec<&EncryptionKeyPair> = old_epoch_keypairs
499 .iter()
500 .chain(leaf_node_keypairs.iter())
501 .collect();
502 diff.decrypt_path(
503 provider.crypto(),
504 &decryption_keypairs,
505 self.own_leaf_index(),
506 sender_index,
507 path.nodes(),
508 &apply_proposals_values.exclusion_list(),
509 )?
510 };
511
512 let new_leaf_keypair_option = if is_sibling_resync {
518 None
519 } else if let Some(leaf) = diff.leaf(self.own_leaf_index()) {
520 leaf_node_keypairs.into_iter().find_map(|keypair| {
521 if leaf.encryption_key() == keypair.public_key() {
522 Some(keypair)
523 } else {
524 None
525 }
526 })
527 } else {
528 debug_assert!(false);
530 None
531 };
532
533 let update_path_leaf_node = Some(path.leaf_node().clone());
537 debug_assert_eq!(diff.leaf(sender_index), path.leaf_node().into());
538
539 (
540 commit_secret,
541 new_keypairs,
542 new_leaf_keypair_option,
543 update_path_leaf_node,
544 )
545 } else {
546 if apply_proposals_values.path_required {
547 return Err(StageCommitError::RequiredPathNotFound);
549 }
550
551 diff.update_group_context(
553 provider.crypto(),
554 apply_proposals_values.extensions.clone(),
555 )?;
556
557 (CommitSecret::zero_secret(ciphersuite), vec![], None, None)
558 };
559
560 diff.update_confirmed_transcript_hash(provider.crypto(), mls_content)?;
562
563 let received_confirmation_tag = mls_content
564 .confirmation_tag()
565 .ok_or(StageCommitError::ConfirmationTagMissing)?;
566
567 let serialized_provisional_group_context = diff
568 .group_context()
569 .tls_serialize_detached()
570 .map_err(LibraryError::missing_bound_check)?;
571
572 #[cfg(feature = "virtual-clients-draft")]
573 let provisional_own_leaf_index = if is_sibling_resync {
574 sender_index
575 } else {
576 self.own_leaf_index()
577 };
578 #[cfg(not(feature = "virtual-clients-draft"))]
579 let provisional_own_leaf_index = self.own_leaf_index();
580
581 let EpochSecretsResult {
582 epoch_secrets,
583 #[cfg(feature = "extensions-draft")]
584 application_exporter,
585 } = self
586 .derive_epoch_secrets(
587 provider,
588 apply_proposals_values,
589 self.group_epoch_secrets(),
590 commit_secret,
591 &serialized_provisional_group_context,
592 #[cfg(feature = "virtual-clients-draft")]
593 vc_external_init_secret.as_ref(),
594 )
595 .await?;
596 let (provisional_group_secrets, provisional_message_secrets) = epoch_secrets.split_secrets(
597 serialized_provisional_group_context,
598 diff.tree_size(),
599 provisional_own_leaf_index,
600 );
601
602 let own_confirmation_tag = provisional_message_secrets
605 .confirmation_key()
606 .tag(
607 provider.crypto(),
608 self.ciphersuite(),
609 diff.group_context().confirmed_transcript_hash(),
610 )
611 .map_err(LibraryError::unexpected_crypto_error)?;
612 if &own_confirmation_tag != received_confirmation_tag {
613 log::error!("Confirmation tag mismatch");
614 log_crypto!(trace, " Got: {:x?}", received_confirmation_tag);
615 log_crypto!(trace, " Expected: {:x?}", own_confirmation_tag);
616 if !crate::skip_validation::is_disabled::confirmation_tag() {
623 return Err(StageCommitError::ConfirmationTagMismatch);
624 }
625 }
626
627 diff.update_interim_transcript_hash(ciphersuite, provider.crypto(), own_confirmation_tag)?;
628
629 let staged_diff = diff.into_staged_diff(provider.crypto(), ciphersuite)?;
630 #[cfg(feature = "extensions-draft")]
631 let application_export_tree = ApplicationExportTree::new(application_exporter);
632 #[cfg(feature = "virtual-clients-draft")]
633 let new_own_leaf_index = is_sibling_resync.then_some(provisional_own_leaf_index);
634 let staged_commit_state =
635 StagedCommitState::GroupMember(Box::new(MemberStagedCommitState::new(
636 provisional_group_secrets,
637 provisional_message_secrets,
638 staged_diff,
639 new_keypairs,
640 new_leaf_keypair_option,
641 update_path_leaf_node,
642 #[cfg(feature = "extensions-draft")]
643 application_export_tree,
644 #[cfg(feature = "virtual-clients-draft")]
645 new_own_leaf_index,
646 )));
647 #[cfg_attr(not(feature = "virtual-clients-draft"), allow(unused_mut))]
648 let mut staged_commit = StagedCommit::new(
649 proposal_queue,
650 staged_commit_state,
651 #[cfg(feature = "virtual-clients-draft")]
652 vc_derivation_epoch_id,
653 );
654 #[cfg(feature = "virtual-clients-draft")]
655 {
656 staged_commit.marks_new_vc_derivation_epoch = marks_new_vc_derivation_epoch;
657 }
658
659 Ok(staged_commit)
660 }
661
662 #[cfg(feature = "virtual-clients-draft")]
684 #[expect(clippy::too_many_arguments)]
685 fn recreate_path_for_own_commit(
686 &self,
687 diff: &PublicGroupDiff,
688 path: &crate::treesync::treekem::UpdatePath,
689 group_ciphersuite: openmls_traits::types::Ciphersuite,
690 group_id: &crate::prelude::GroupId,
691 crypto: &impl OpenMlsCrypto,
692 sender_index: LeafNodeIndex,
693 operation_secret: crate::components::vc_derivation_info::OperationSecret,
694 ) -> Result<(Vec<EncryptionKeyPair>, CommitSecret), StageCommitError> {
695 use crate::components::vc_derivation_info::VirtualClientsError;
696
697 let target_operation_secret =
698 operation_secret.derive_target_operation_secret(crypto, group_ciphersuite, group_id)?;
699
700 let path_secret = target_operation_secret
701 .derive_path_generation_secret(crypto, group_ciphersuite)?
702 .into();
703 let (encryption_key_pairs, commit_secret) =
704 diff.recreate_path_from_path_secret(crypto, path_secret, sender_index, path.nodes())?;
705
706 let leaf_keypair = target_operation_secret
709 .derive_encryption_key_secret(crypto, group_ciphersuite)?
710 .generate_encryption_key_pair(crypto, group_ciphersuite)?;
711 drop(target_operation_secret);
712 if leaf_keypair.public_key() != path.leaf_node().encryption_key() {
713 return Err(VirtualClientsError::EncryptionKeyMismatch.into());
714 }
715
716 let mut keypairs = Vec::with_capacity(1 + encryption_key_pairs.len());
720 keypairs.push(leaf_keypair);
721 keypairs.extend(encryption_key_pairs);
722 Ok((keypairs, commit_secret))
723 }
724
725 #[cfg(feature = "virtual-clients-draft")]
737 pub(crate) fn commit_creates_vc_derivation_epoch(&self, staged_commit: &StagedCommit) -> bool {
738 self.is_emulation_group()
739 && matches!(staged_commit.state, StagedCommitState::GroupMember(_))
740 && (staged_commit.marks_new_vc_derivation_epoch || staged_commit.changes_membership())
741 }
742
743 #[openmls_traits::maybe_async]
752 pub(crate) async fn merge_commit<Provider: OpenMlsProvider>(
753 &mut self,
754 provider: &Provider,
755 staged_commit: StagedCommit,
756 ) -> Result<(), MergeCommitError<Provider::StorageError>> {
757 let old_epoch_keypairs = self
760 .read_epoch_keypairs(provider.storage())
761 .await
762 .map_err(MergeCommitError::StorageError)?;
763
764 #[cfg(feature = "virtual-clients-draft")]
765 let creates_vc_derivation_epoch = self.commit_creates_vc_derivation_epoch(&staged_commit);
766
767 match staged_commit.state {
768 StagedCommitState::PublicState(staged_state) => {
769 self.public_group
770 .merge_diff(staged_state.into_staged_diff());
771 self.store(provider.storage())
772 .await
773 .map_err(MergeCommitError::StorageError)?;
774 Ok(())
775 }
776 StagedCommitState::GroupMember(state) => {
777 let past_epoch = self.context().epoch();
779 let leaves = self.public_group().members().collect();
781 self.group_epoch_secrets = state.group_epoch_secrets;
784
785 let old_message_secrets = self
787 .message_secrets_store
788 .replace_current_message_secrets(state.message_secrets);
789 self.message_secrets_store.add_past_epoch_tree(
790 past_epoch,
791 old_message_secrets,
792 leaves,
793 );
794
795 self.public_group.merge_diff(state.staged_diff);
796
797 #[cfg(feature = "virtual-clients-draft")]
798 let previous_own_leaf_index = self.own_leaf_index;
799
800 #[cfg(feature = "virtual-clients-draft")]
805 if let Some(new_idx) = state.new_own_leaf_index {
806 self.own_leaf_index = new_idx;
807 }
808
809 #[cfg(feature = "extensions-draft")]
814 {
815 #[cfg_attr(not(feature = "virtual-clients-draft"), allow(unused_mut))]
821 let mut application_export_tree = state.application_export_tree;
822
823 #[cfg(feature = "virtual-clients-draft")]
826 if creates_vc_derivation_epoch {
827 crate::components::vc_derivation_info::register_vc_derivation_epoch(
828 provider.crypto(),
829 provider.storage(),
830 application_export_tree.as_mut(),
831 crate::components::vc_derivation_info::VcDerivationEpochParams::for_public_group(
832 self.public_group(),
833 self.own_leaf_index(),
834 self.mls_group_config
835 .vc_derivation_epoch_retention_policy()
836 .clone(),
837 ),
838 )
839 .await?;
840 }
841
842 if let Some(application_export_tree) = application_export_tree {
843 use openmls_traits::storage::StorageProvider as _;
845 provider
846 .storage()
847 .write_application_export_tree(
848 self.group_id(),
849 &application_export_tree,
850 )
851 .await
852 .map_err(MergeCommitError::StorageError)?;
853
854 self.application_export_tree = Some(application_export_tree);
855 }
856 }
857
858 let leaf_keypair = if let Some(keypair) = &state.new_leaf_keypair_option {
859 vec![keypair.clone()]
860 } else {
861 vec![]
862 };
863
864 let new_owned_encryption_keys = self
866 .public_group()
867 .owned_encryption_keys(self.own_leaf_index());
868 let epoch_keypairs: Vec<EncryptionKeyPair> = old_epoch_keypairs
870 .into_iter()
871 .chain(state.new_keypairs)
872 .chain(leaf_keypair)
873 .filter(|keypair| new_owned_encryption_keys.contains(keypair.public_key()))
874 .collect();
875
876 debug_assert_eq!(new_owned_encryption_keys.len(), epoch_keypairs.len());
878 if new_owned_encryption_keys.len() != epoch_keypairs.len() {
879 return Err(LibraryError::custom(
880 "We should have all the private key material we need.",
881 )
882 .into());
883 }
884
885 let storage = provider.storage();
887 let group_id = self.group_id();
888
889 self.public_group
890 .store(storage)
891 .await
892 .map_err(MergeCommitError::StorageError)?;
893 storage
894 .write_own_leaf_index(group_id, &self.own_leaf_index)
895 .await
896 .map_err(MergeCommitError::StorageError)?;
897 storage
898 .write_group_epoch_secrets(group_id, &self.group_epoch_secrets)
899 .await
900 .map_err(MergeCommitError::StorageError)?;
901 storage
902 .write_message_secrets(group_id, &self.message_secrets_store)
903 .await
904 .map_err(MergeCommitError::StorageError)?;
905
906 self.store_epoch_keypairs(storage, epoch_keypairs.as_slice())
908 .await
909 .map_err(MergeCommitError::StorageError)?;
910
911 self.delete_previous_epoch_keypairs(
913 storage,
914 #[cfg(feature = "virtual-clients-draft")]
915 previous_own_leaf_index,
916 )
917 .await
918 .map_err(MergeCommitError::StorageError)?;
919 if let Some(keypair) = state.new_leaf_keypair_option {
920 keypair
921 .delete(storage)
922 .await
923 .map_err(MergeCommitError::StorageError)?;
924 }
925
926 storage
928 .clear_proposal_queue::<GroupId, ProposalRef>(group_id)
929 .await
930 .map_err(MergeCommitError::StorageError)?;
931 self.proposal_store_mut().empty();
932
933 Ok(())
934 }
935 }
936 }
937}
938
939#[derive(Debug, Serialize, Deserialize)]
940#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
941pub(crate) enum StagedCommitState {
942 PublicState(Box<PublicStagedCommitState>),
943 GroupMember(Box<MemberStagedCommitState>),
945}
946
947#[derive(Debug, Serialize, Deserialize)]
949#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
950pub struct StagedCommit {
951 pub staged_proposal_queue: ProposalQueue,
953 pub(super) state: StagedCommitState,
955 #[cfg(feature = "virtual-clients-draft")]
958 #[serde(default, alias = "vc_emulation_epoch_id")]
959 pub(super) vc_derivation_epoch_id: Option<crate::components::vc_derivation_info::EpochId>,
961 #[cfg(feature = "virtual-clients-draft")]
967 #[serde(default)]
968 pub(super) marks_new_vc_derivation_epoch: bool,
969}
970
971impl StagedCommit {
972 pub(crate) fn new(
975 staged_proposal_queue: ProposalQueue,
976 state: StagedCommitState,
977 #[cfg(feature = "virtual-clients-draft")] vc_derivation_epoch_id: Option<
978 crate::components::vc_derivation_info::EpochId,
979 >,
980 ) -> Self {
981 StagedCommit {
982 staged_proposal_queue,
983 state,
984 #[cfg(feature = "virtual-clients-draft")]
985 vc_derivation_epoch_id,
986 #[cfg(feature = "virtual-clients-draft")]
987 marks_new_vc_derivation_epoch: false,
988 }
989 }
990
991 #[cfg(feature = "virtual-clients-draft")]
994 fn changes_membership(&self) -> bool {
995 self.staged_proposal_queue
996 .queued_proposals()
997 .any(|queued| match queued.proposal() {
998 Proposal::Add(_)
999 | Proposal::Remove(_)
1000 | Proposal::SelfRemove
1001 | Proposal::ExternalInit(_) => true,
1002 Proposal::Update(_)
1003 | Proposal::PreSharedKey(_)
1004 | Proposal::ReInit(_)
1005 | Proposal::GroupContextExtensions(_)
1006 | Proposal::AppDataUpdate(_)
1007 | Proposal::AppEphemeral(_)
1008 | Proposal::Custom(_) => false,
1009 })
1010 }
1011
1012 pub fn epoch(&self) -> GroupEpoch {
1014 self.group_context().epoch()
1015 }
1016
1017 pub fn export_ratchet_tree(
1019 &self,
1020 crypto: &impl OpenMlsCrypto,
1021 original_tree: RatchetTree,
1022 ) -> Result<Option<RatchetTree>, TreeSyncFromNodesError> {
1023 match &self.state {
1024 StagedCommitState::PublicState(_public_staged_commit_state) => Ok(None),
1025 StagedCommitState::GroupMember(member_staged_commit_state) => Ok(Some(
1026 member_staged_commit_state.staged_diff.export_ratchet_tree(
1027 crypto,
1028 self.group_context().ciphersuite(),
1029 original_tree,
1030 )?,
1031 )),
1032 }
1033 }
1034
1035 pub fn add_proposals(&self) -> impl Iterator<Item = QueuedAddProposal<'_>> {
1037 self.staged_proposal_queue.add_proposals()
1038 }
1039
1040 pub fn remove_proposals(&self) -> impl Iterator<Item = QueuedRemoveProposal<'_>> {
1042 self.staged_proposal_queue.remove_proposals()
1043 }
1044
1045 pub fn update_proposals(&self) -> impl Iterator<Item = QueuedUpdateProposal<'_>> {
1047 self.staged_proposal_queue.update_proposals()
1048 }
1049
1050 pub fn psk_proposals(&self) -> impl Iterator<Item = QueuedPskProposal<'_>> {
1052 self.staged_proposal_queue.psk_proposals()
1053 }
1054
1055 pub fn reinit_proposal(&self) -> Option<&ReInitProposal> {
1061 self.staged_proposal_queue.reinit_proposal()
1062 }
1063
1064 #[cfg(feature = "extensions-draft")]
1065 pub fn queued_app_ephemeral_proposals(
1068 &self,
1069 ) -> impl Iterator<Item = QueuedAppEphemeralProposal<'_>> {
1070 self.staged_proposal_queue.app_ephemeral_proposals()
1071 }
1072 #[cfg(feature = "extensions-draft")]
1074 pub fn app_data_update_proposals(
1077 &self,
1078 ) -> impl Iterator<Item = QueuedAppDataUpdateProposal<'_>> {
1079 self.staged_proposal_queue.app_data_update_proposals()
1080 }
1081
1082 pub fn queued_proposals(&self) -> impl Iterator<Item = &QueuedProposal> {
1084 self.staged_proposal_queue.queued_proposals()
1085 }
1086
1087 pub fn update_path_leaf_node(&self) -> Option<&LeafNode> {
1089 match self.state {
1090 StagedCommitState::PublicState(ref public_state) => {
1091 public_state.update_path_leaf_node()
1092 }
1093 StagedCommitState::GroupMember(ref group_member_state) => {
1094 group_member_state.update_path_leaf_node.as_ref()
1095 }
1096 }
1097 }
1098
1099 pub fn credentials_to_verify(&self) -> impl Iterator<Item = &Credential> {
1101 let update_path_leaf_node_cred = if let Some(node) = self.update_path_leaf_node() {
1102 vec![node.credential()]
1103 } else {
1104 vec![]
1105 };
1106
1107 update_path_leaf_node_cred
1108 .into_iter()
1109 .chain(
1110 self.queued_proposals()
1111 .flat_map(|proposal: &QueuedProposal| match proposal.proposal() {
1112 Proposal::Update(update_proposal) => {
1113 vec![update_proposal.leaf_node().credential()].into_iter()
1114 }
1115 Proposal::Add(add_proposal) => {
1116 vec![add_proposal.key_package().leaf_node().credential()].into_iter()
1117 }
1118 Proposal::GroupContextExtensions(gce_proposal) => gce_proposal
1119 .extensions()
1120 .iter()
1121 .flat_map(|extension| {
1122 match extension {
1123 Extension::ExternalSenders(external_senders) => {
1124 external_senders
1125 .iter()
1126 .map(|external_sender| external_sender.credential())
1127 .collect()
1128 }
1129 _ => vec![],
1130 }
1131 .into_iter()
1132 })
1133 .collect::<Vec<_>>()
1140 .into_iter(),
1141 _ => vec![].into_iter(),
1142 }),
1143 )
1144 }
1145
1146 pub fn self_removed(&self) -> bool {
1155 matches!(self.state, StagedCommitState::PublicState(_))
1156 }
1157
1158 pub fn group_context(&self) -> &GroupContext {
1160 match self.state {
1161 StagedCommitState::PublicState(ref ps) => ps.staged_diff().group_context(),
1162 StagedCommitState::GroupMember(ref gm) => gm.group_context(),
1163 }
1164 }
1165 pub(crate) fn into_state(self) -> StagedCommitState {
1167 self.state
1168 }
1169
1170 pub fn epoch_authenticator(&self) -> Option<&EpochAuthenticator> {
1174 if let StagedCommitState::GroupMember(ref gm) = self.state {
1175 Some(gm.group_epoch_secrets.epoch_authenticator())
1176 } else {
1177 None
1178 }
1179 }
1180
1181 pub fn resumption_psk_secret(&self) -> Option<&ResumptionPskSecret> {
1185 if let StagedCommitState::GroupMember(ref gm) = self.state {
1186 Some(gm.group_epoch_secrets.resumption_psk())
1187 } else {
1188 None
1189 }
1190 }
1191
1192 #[cfg(feature = "extensions-draft")]
1200 pub fn safe_export_secret(
1201 &mut self,
1202 crypto: &impl OpenMlsCrypto,
1203 component_id: ComponentId,
1204 ) -> Result<ExportedSecret<StagedCommitSafeExport>, StagedSafeExportSecretError> {
1205 let ciphersuite = self.group_context().ciphersuite();
1206 let StagedCommitState::GroupMember(ref mut staged_commit) = self.state else {
1207 return Err(StagedSafeExportSecretError::NotGroupMember);
1208 };
1209 let Some(application_export_tree) = staged_commit.application_export_tree.as_mut() else {
1210 return Err(StagedSafeExportSecretError::Unsupported);
1211 };
1212 let secret =
1213 application_export_tree.safe_export_secret(crypto, ciphersuite, component_id)?;
1214 Ok(ExportedSecret::new(secret))
1215 }
1216
1217 pub fn export_secret<CryptoProvider: OpenMlsCrypto>(
1225 &self,
1226 crypto: &CryptoProvider,
1227 label: &str,
1228 context: &[u8],
1229 key_length: usize,
1230 ) -> Result<ExportedSecret<StagedCommitExport>, ExportSecretError> {
1231 if key_length > u16::MAX as usize {
1232 log::error!("Got a key that is larger than u16::MAX");
1233 return Err(ExportSecretError::KeyLengthTooLong);
1234 }
1235
1236 match &self.state {
1237 StagedCommitState::PublicState(_public_staged_commit_state) => Err(
1238 ExportSecretError::GroupStateError(MlsGroupStateError::UseAfterEviction),
1239 ),
1240 StagedCommitState::GroupMember(member_staged_commit_state) => Ok(ExportedSecret::new(
1241 member_staged_commit_state
1242 .group_epoch_secrets
1243 .exporter_secret()
1244 .derive_exported_secret(
1245 self.group_context().ciphersuite(),
1246 crypto,
1247 label,
1248 context,
1249 key_length,
1250 )
1251 .map_err(LibraryError::unexpected_crypto_error)?,
1252 )),
1253 }
1254 }
1255}
1256
1257#[derive(Debug, Serialize, Deserialize)]
1259#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
1260pub(crate) struct MemberStagedCommitState {
1261 group_epoch_secrets: GroupEpochSecrets,
1262 message_secrets: MessageSecrets,
1263 staged_diff: StagedPublicGroupDiff,
1264 new_keypairs: Vec<EncryptionKeyPair>,
1265 new_leaf_keypair_option: Option<EncryptionKeyPair>,
1266 update_path_leaf_node: Option<LeafNode>,
1267 #[cfg(feature = "extensions-draft")]
1268 #[serde(default)]
1269 application_export_tree: Option<ApplicationExportTree>,
1272 #[cfg(feature = "virtual-clients-draft")]
1277 #[serde(default)]
1278 new_own_leaf_index: Option<LeafNodeIndex>,
1279}
1280
1281impl MemberStagedCommitState {
1282 #[allow(clippy::too_many_arguments)]
1283 pub(crate) fn new(
1284 group_epoch_secrets: GroupEpochSecrets,
1285 message_secrets: MessageSecrets,
1286 staged_diff: StagedPublicGroupDiff,
1287 new_keypairs: Vec<EncryptionKeyPair>,
1288 new_leaf_keypair_option: Option<EncryptionKeyPair>,
1289 update_path_leaf_node: Option<LeafNode>,
1290 #[cfg(feature = "extensions-draft")] application_export_tree: ApplicationExportTree,
1291 #[cfg(feature = "virtual-clients-draft")] new_own_leaf_index: Option<LeafNodeIndex>,
1292 ) -> Self {
1293 Self {
1294 group_epoch_secrets,
1295 message_secrets,
1296 staged_diff,
1297 new_keypairs,
1298 new_leaf_keypair_option,
1299 update_path_leaf_node,
1300 #[cfg(feature = "extensions-draft")]
1301 application_export_tree: Some(application_export_tree),
1302 #[cfg(feature = "virtual-clients-draft")]
1303 new_own_leaf_index,
1304 }
1305 }
1306
1307 pub(crate) fn group_context(&self) -> &GroupContext {
1309 self.staged_diff.group_context()
1310 }
1311}
1312
1313#[cfg(all(test, feature = "virtual-clients-draft"))]
1314mod tests {
1315 use super::validate_vc_external_init_secret;
1316 use crate::{
1317 components::vc_derivation_info::VirtualClientsError, group::errors::StageCommitError,
1318 };
1319
1320 #[test]
1325 fn external_init_secret_presence_is_validated() {
1326 let malformed: Result<(), StageCommitError> =
1327 Err(VirtualClientsError::DerivationInfoMalformed.into());
1328
1329 assert_eq!(
1331 validate_vc_external_init_secret(true, true, false),
1332 malformed
1333 );
1334 assert_eq!(
1336 validate_vc_external_init_secret(false, false, true),
1337 malformed
1338 );
1339 assert_eq!(
1340 validate_vc_external_init_secret(true, false, true),
1341 malformed
1342 );
1343
1344 assert_eq!(validate_vc_external_init_secret(true, true, true), Ok(()));
1347 assert_eq!(validate_vc_external_init_secret(false, true, false), Ok(()));
1348 assert_eq!(
1349 validate_vc_external_init_secret(false, false, false),
1350 Ok(())
1351 );
1352 }
1353}