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, 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 #[cfg(feature = "extensions-draft")]
1029 pub fn queued_app_ephemeral_proposals(
1032 &self,
1033 ) -> impl Iterator<Item = QueuedAppEphemeralProposal<'_>> {
1034 self.staged_proposal_queue.app_ephemeral_proposals()
1035 }
1036 #[cfg(feature = "extensions-draft")]
1038 pub fn app_data_update_proposals(
1041 &self,
1042 ) -> impl Iterator<Item = QueuedAppDataUpdateProposal<'_>> {
1043 self.staged_proposal_queue.app_data_update_proposals()
1044 }
1045
1046 pub fn queued_proposals(&self) -> impl Iterator<Item = &QueuedProposal> {
1048 self.staged_proposal_queue.queued_proposals()
1049 }
1050
1051 pub fn update_path_leaf_node(&self) -> Option<&LeafNode> {
1053 match self.state {
1054 StagedCommitState::PublicState(ref public_state) => {
1055 public_state.update_path_leaf_node()
1056 }
1057 StagedCommitState::GroupMember(ref group_member_state) => {
1058 group_member_state.update_path_leaf_node.as_ref()
1059 }
1060 }
1061 }
1062
1063 pub fn credentials_to_verify(&self) -> impl Iterator<Item = &Credential> {
1065 let update_path_leaf_node_cred = if let Some(node) = self.update_path_leaf_node() {
1066 vec![node.credential()]
1067 } else {
1068 vec![]
1069 };
1070
1071 update_path_leaf_node_cred
1072 .into_iter()
1073 .chain(
1074 self.queued_proposals()
1075 .flat_map(|proposal: &QueuedProposal| match proposal.proposal() {
1076 Proposal::Update(update_proposal) => {
1077 vec![update_proposal.leaf_node().credential()].into_iter()
1078 }
1079 Proposal::Add(add_proposal) => {
1080 vec![add_proposal.key_package().leaf_node().credential()].into_iter()
1081 }
1082 Proposal::GroupContextExtensions(gce_proposal) => gce_proposal
1083 .extensions()
1084 .iter()
1085 .flat_map(|extension| {
1086 match extension {
1087 Extension::ExternalSenders(external_senders) => {
1088 external_senders
1089 .iter()
1090 .map(|external_sender| external_sender.credential())
1091 .collect()
1092 }
1093 _ => vec![],
1094 }
1095 .into_iter()
1096 })
1097 .collect::<Vec<_>>()
1104 .into_iter(),
1105 _ => vec![].into_iter(),
1106 }),
1107 )
1108 }
1109
1110 pub fn self_removed(&self) -> bool {
1119 matches!(self.state, StagedCommitState::PublicState(_))
1120 }
1121
1122 pub fn group_context(&self) -> &GroupContext {
1124 match self.state {
1125 StagedCommitState::PublicState(ref ps) => ps.staged_diff().group_context(),
1126 StagedCommitState::GroupMember(ref gm) => gm.group_context(),
1127 }
1128 }
1129 pub(crate) fn into_state(self) -> StagedCommitState {
1131 self.state
1132 }
1133
1134 pub fn epoch_authenticator(&self) -> Option<&EpochAuthenticator> {
1138 if let StagedCommitState::GroupMember(ref gm) = self.state {
1139 Some(gm.group_epoch_secrets.epoch_authenticator())
1140 } else {
1141 None
1142 }
1143 }
1144
1145 pub fn resumption_psk_secret(&self) -> Option<&ResumptionPskSecret> {
1149 if let StagedCommitState::GroupMember(ref gm) = self.state {
1150 Some(gm.group_epoch_secrets.resumption_psk())
1151 } else {
1152 None
1153 }
1154 }
1155
1156 #[cfg(feature = "extensions-draft")]
1164 pub fn safe_export_secret(
1165 &mut self,
1166 crypto: &impl OpenMlsCrypto,
1167 component_id: ComponentId,
1168 ) -> Result<ExportedSecret<StagedCommitSafeExport>, StagedSafeExportSecretError> {
1169 let ciphersuite = self.group_context().ciphersuite();
1170 let StagedCommitState::GroupMember(ref mut staged_commit) = self.state else {
1171 return Err(StagedSafeExportSecretError::NotGroupMember);
1172 };
1173 let Some(application_export_tree) = staged_commit.application_export_tree.as_mut() else {
1174 return Err(StagedSafeExportSecretError::Unsupported);
1175 };
1176 let secret =
1177 application_export_tree.safe_export_secret(crypto, ciphersuite, component_id)?;
1178 Ok(ExportedSecret::new(secret))
1179 }
1180
1181 pub fn export_secret<CryptoProvider: OpenMlsCrypto>(
1189 &self,
1190 crypto: &CryptoProvider,
1191 label: &str,
1192 context: &[u8],
1193 key_length: usize,
1194 ) -> Result<ExportedSecret<StagedCommitExport>, ExportSecretError> {
1195 if key_length > u16::MAX as usize {
1196 log::error!("Got a key that is larger than u16::MAX");
1197 return Err(ExportSecretError::KeyLengthTooLong);
1198 }
1199
1200 match &self.state {
1201 StagedCommitState::PublicState(_public_staged_commit_state) => Err(
1202 ExportSecretError::GroupStateError(MlsGroupStateError::UseAfterEviction),
1203 ),
1204 StagedCommitState::GroupMember(member_staged_commit_state) => Ok(ExportedSecret::new(
1205 member_staged_commit_state
1206 .group_epoch_secrets
1207 .exporter_secret()
1208 .derive_exported_secret(
1209 self.group_context().ciphersuite(),
1210 crypto,
1211 label,
1212 context,
1213 key_length,
1214 )
1215 .map_err(LibraryError::unexpected_crypto_error)?,
1216 )),
1217 }
1218 }
1219}
1220
1221#[derive(Debug, Serialize, Deserialize)]
1223#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
1224pub(crate) struct MemberStagedCommitState {
1225 group_epoch_secrets: GroupEpochSecrets,
1226 message_secrets: MessageSecrets,
1227 staged_diff: StagedPublicGroupDiff,
1228 new_keypairs: Vec<EncryptionKeyPair>,
1229 new_leaf_keypair_option: Option<EncryptionKeyPair>,
1230 update_path_leaf_node: Option<LeafNode>,
1231 #[cfg(feature = "extensions-draft")]
1232 #[serde(default)]
1233 application_export_tree: Option<ApplicationExportTree>,
1236 #[cfg(feature = "virtual-clients-draft")]
1241 #[serde(default)]
1242 new_own_leaf_index: Option<LeafNodeIndex>,
1243}
1244
1245impl MemberStagedCommitState {
1246 #[allow(clippy::too_many_arguments)]
1247 pub(crate) fn new(
1248 group_epoch_secrets: GroupEpochSecrets,
1249 message_secrets: MessageSecrets,
1250 staged_diff: StagedPublicGroupDiff,
1251 new_keypairs: Vec<EncryptionKeyPair>,
1252 new_leaf_keypair_option: Option<EncryptionKeyPair>,
1253 update_path_leaf_node: Option<LeafNode>,
1254 #[cfg(feature = "extensions-draft")] application_export_tree: ApplicationExportTree,
1255 #[cfg(feature = "virtual-clients-draft")] new_own_leaf_index: Option<LeafNodeIndex>,
1256 ) -> Self {
1257 Self {
1258 group_epoch_secrets,
1259 message_secrets,
1260 staged_diff,
1261 new_keypairs,
1262 new_leaf_keypair_option,
1263 update_path_leaf_node,
1264 #[cfg(feature = "extensions-draft")]
1265 application_export_tree: Some(application_export_tree),
1266 #[cfg(feature = "virtual-clients-draft")]
1267 new_own_leaf_index,
1268 }
1269 }
1270
1271 pub(crate) fn group_context(&self) -> &GroupContext {
1273 self.staged_diff.group_context()
1274 }
1275}
1276
1277#[cfg(all(test, feature = "virtual-clients-draft"))]
1278mod tests {
1279 use super::validate_vc_external_init_secret;
1280 use crate::{
1281 components::vc_derivation_info::VirtualClientsError, group::errors::StageCommitError,
1282 };
1283
1284 #[test]
1289 fn external_init_secret_presence_is_validated() {
1290 let malformed: Result<(), StageCommitError> =
1291 Err(VirtualClientsError::DerivationInfoMalformed.into());
1292
1293 assert_eq!(
1295 validate_vc_external_init_secret(true, true, false),
1296 malformed
1297 );
1298 assert_eq!(
1300 validate_vc_external_init_secret(false, false, true),
1301 malformed
1302 );
1303 assert_eq!(
1304 validate_vc_external_init_secret(true, false, true),
1305 malformed
1306 );
1307
1308 assert_eq!(validate_vc_external_init_secret(true, true, true), Ok(()));
1311 assert_eq!(validate_vc_external_init_secret(false, true, false), Ok(()));
1312 assert_eq!(
1313 validate_vc_external_init_secret(false, false, false),
1314 Ok(())
1315 );
1316 }
1317}