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;
14use super::{
15 super::errors::*, load_psks, Credential, Extension, GroupContext, GroupEpochSecrets, GroupId,
16 JoinerSecret, KeySchedule, LeafNode, LibraryError, MessageSecrets, MlsGroup, MlsGroupState,
17 OpenMlsProvider, PendingCommitState, Proposal, ProposalQueue, PskSecret, QueuedProposal,
18};
19use crate::group::diff::PublicGroupDiff;
20use crate::group::GroupEpoch;
21use crate::messages::ConfirmationTag;
22use crate::prelude::{Commit, LeafNodeIndex};
23#[cfg(feature = "extensions-draft")]
24use crate::{component::ComponentId, schedule::application_export_tree::ApplicationExportTree};
25
26use crate::treesync::errors::TreeSyncFromNodesError;
27use crate::treesync::RatchetTree;
28use crate::{
29 ciphersuite::{hash_ref::ProposalRef, Secret},
30 framing::mls_auth_content::AuthenticatedContent,
31 group::public_group::{
32 diff::{apply_proposals::ApplyProposalsValues, StagedPublicGroupDiff},
33 staged_commit::PublicStagedCommitState,
34 },
35 schedule::{
36 CommitSecret, EpochAuthenticator, EpochSecretsResult, InitSecret, PreSharedKeyId,
37 ResumptionPskSecret,
38 },
39 treesync::node::encryption_keys::EncryptionKeyPair,
40};
41
42#[cfg(feature = "extensions-draft")]
43use super::proposal_store::{QueuedAppDataUpdateProposal, QueuedAppEphemeralProposal};
44#[cfg(feature = "extensions-draft")]
45use crate::prelude::processing::AppDataUpdates;
46
47#[cfg(feature = "virtual-clients-draft")]
48fn validate_vc_external_init_secret(
49 is_sibling_resync: bool,
50 has_external_init_proposal: bool,
51 has_vc_external_init_secret: bool,
52) -> Result<(), StageCommitError> {
53 if is_sibling_resync && !has_vc_external_init_secret {
54 return Err(
55 crate::components::vc_derivation_info::VirtualClientsError::DerivationInfoMalformed
56 .into(),
57 );
58 }
59 if has_vc_external_init_secret && !has_external_init_proposal {
60 return Err(
61 crate::components::vc_derivation_info::VirtualClientsError::DerivationInfoMalformed
62 .into(),
63 );
64 }
65 Ok(())
66}
67
68impl MlsGroup {
69 pub(crate) fn matches_pending_commit(&self, received_tag: &ConfirmationTag) -> bool {
78 let MlsGroupState::PendingCommit(pending_commit_state) = &self.group_state else {
79 return false;
80 };
81 let PendingCommitState::Member(staged_commit) = pending_commit_state.as_ref() else {
82 return false;
83 };
84 let StagedCommitState::GroupMember(member_state) = &staged_commit.state else {
85 return false;
86 };
87 member_state.staged_diff.confirmation_tag() == received_tag
88 }
89
90 fn derive_epoch_secrets(
91 &self,
92 provider: &impl OpenMlsProvider,
93 apply_proposals_values: ApplyProposalsValues,
94 epoch_secrets: &GroupEpochSecrets,
95 commit_secret: CommitSecret,
96 serialized_provisional_group_context: &[u8],
97 #[cfg(feature = "virtual-clients-draft")] vc_external_init_secret: Option<
98 &crate::components::vc_derivation_info::ExternalInitSecret,
99 >,
100 ) -> Result<EpochSecretsResult, StageCommitError> {
101 let joiner_secret = if let Some(ref external_init_proposal) =
104 apply_proposals_values.external_init_proposal_option
105 {
106 #[cfg(feature = "virtual-clients-draft")]
113 let carried_init_secret = vc_external_init_secret
114 .map(|carried| InitSecret::from(Secret::from_slice(carried.as_slice())));
115 #[cfg(not(feature = "virtual-clients-draft"))]
116 let carried_init_secret: Option<InitSecret> = None;
117
118 let init_secret = match carried_init_secret {
119 Some(init_secret) => init_secret,
120 None => {
121 let external_priv = epoch_secrets
122 .external_secret()
123 .derive_external_keypair(provider.crypto(), self.ciphersuite())
124 .map_err(LibraryError::unexpected_crypto_error)?
125 .private;
126 InitSecret::from_kem_output(
127 provider.crypto(),
128 self.ciphersuite(),
129 self.version(),
130 &external_priv,
131 external_init_proposal.kem_output(),
132 )?
133 }
134 };
135 JoinerSecret::new(
136 provider.crypto(),
137 self.ciphersuite(),
138 commit_secret,
139 &init_secret,
140 serialized_provisional_group_context,
141 )
142 .map_err(LibraryError::unexpected_crypto_error)?
143 } else {
144 JoinerSecret::new(
145 provider.crypto(),
146 self.ciphersuite(),
147 commit_secret,
148 epoch_secrets.init_secret(),
149 serialized_provisional_group_context,
150 )
151 .map_err(LibraryError::unexpected_crypto_error)?
152 };
153
154 let psk_secret = {
157 let psks: Vec<(&PreSharedKeyId, Secret)> = load_psks(
158 provider.storage(),
159 &self.resumption_psk_store,
160 &apply_proposals_values.presharedkeys,
161 )?;
162
163 PskSecret::new(provider.crypto(), self.ciphersuite(), psks)?
164 };
165
166 let mut key_schedule = KeySchedule::init(
168 self.ciphersuite(),
169 provider.crypto(),
170 &joiner_secret,
171 psk_secret,
172 )?;
173
174 key_schedule
175 .add_context(provider.crypto(), serialized_provisional_group_context)
176 .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
177 Ok(key_schedule
178 .epoch_secrets(provider.crypto(), self.ciphersuite())
179 .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?)
180 }
181
182 pub(crate) fn stage_commit(
221 &self,
222 mls_content: &AuthenticatedContent,
223 old_epoch_keypairs: Vec<EncryptionKeyPair>,
224 leaf_node_keypairs: Vec<EncryptionKeyPair>,
225 provider: &impl OpenMlsProvider,
226 #[cfg(feature = "virtual-clients-draft")] vc_commit_material: Option<
227 crate::components::vc_derivation_info::VcCommitMaterial,
228 >,
229 ) -> Result<StagedCommit, StageCommitError> {
230 let (commit, proposal_queue, sender_index) = self
231 .public_group
232 .validate_commit(mls_content, provider.crypto())?;
233
234 let mut diff = self.public_group.empty_diff();
237
238 #[cfg(not(feature = "extensions-draft"))]
239 let apply_proposals_values =
240 diff.apply_proposals(&proposal_queue, self.own_leaf_index())?;
241
242 #[cfg(feature = "extensions-draft")]
243 let apply_proposals_values = diff.apply_proposals_with_app_data_updates(
244 &proposal_queue,
245 self.own_leaf_index(),
246 None,
247 )?;
248 self.stage_applied_proposal_values(
249 apply_proposals_values,
250 diff,
251 commit,
252 proposal_queue,
253 sender_index,
254 mls_content,
255 old_epoch_keypairs,
256 leaf_node_keypairs,
257 provider,
258 #[cfg(feature = "virtual-clients-draft")]
259 vc_commit_material,
260 )
261 }
262
263 #[cfg(feature = "extensions-draft")]
264 #[allow(clippy::too_many_arguments)]
265 pub(crate) fn stage_commit_with_app_data_updates(
266 &self,
267 mls_content: &AuthenticatedContent,
268 old_epoch_keypairs: Vec<EncryptionKeyPair>,
269 leaf_node_keypairs: Vec<EncryptionKeyPair>,
270 app_data_dict_updates: Option<AppDataUpdates>,
271 provider: &impl OpenMlsProvider,
272 #[cfg(feature = "virtual-clients-draft")] vc_commit_material: Option<
273 crate::components::vc_derivation_info::VcCommitMaterial,
274 >,
275 ) -> Result<StagedCommit, StageCommitError> {
276 let (commit, proposal_queue, sender_index) = self
277 .public_group
278 .validate_commit(mls_content, provider.crypto())?;
279
280 let mut diff = self.public_group.empty_diff();
283
284 let apply_proposals_values = diff.apply_proposals_with_app_data_updates(
285 &proposal_queue,
286 self.own_leaf_index(),
287 app_data_dict_updates,
288 )?;
289
290 self.stage_applied_proposal_values(
291 apply_proposals_values,
292 diff,
293 commit,
294 proposal_queue,
295 sender_index,
296 mls_content,
297 old_epoch_keypairs,
298 leaf_node_keypairs,
299 provider,
300 #[cfg(feature = "virtual-clients-draft")]
301 vc_commit_material,
302 )
303 }
304
305 #[allow(clippy::too_many_arguments)]
306 fn stage_applied_proposal_values(
307 &self,
308 apply_proposals_values: ApplyProposalsValues,
309 mut diff: PublicGroupDiff,
310 commit: &Commit,
311 proposal_queue: ProposalQueue,
312 sender_index: LeafNodeIndex,
313 mls_content: &AuthenticatedContent,
314 old_epoch_keypairs: Vec<EncryptionKeyPair>,
315 leaf_node_keypairs: Vec<EncryptionKeyPair>,
316 provider: &impl OpenMlsProvider,
317 #[cfg(feature = "virtual-clients-draft")] vc_commit_material: Option<
318 crate::components::vc_derivation_info::VcCommitMaterial,
319 >,
320 ) -> Result<StagedCommit, StageCommitError> {
321 let ciphersuite = self.ciphersuite();
322
323 #[cfg(feature = "virtual-clients-draft")]
328 let (vc_material, vc_emulation_epoch_id, vc_external_init_secret) = match vc_commit_material
329 {
330 Some(material) => (
331 Some(material.operation_secret),
332 Some(material.epoch_id),
333 material.external_init_secret,
334 ),
335 None => (None, None, None),
336 };
337
338 #[cfg(feature = "virtual-clients-draft")]
359 let is_sibling_resync =
360 vc_material.is_some() && matches!(mls_content.sender(), Sender::NewMemberCommit);
361 #[cfg(not(feature = "virtual-clients-draft"))]
362 let is_sibling_resync = false;
363
364 #[cfg(feature = "virtual-clients-draft")]
369 validate_vc_external_init_secret(
370 is_sibling_resync,
371 apply_proposals_values
372 .external_init_proposal_option
373 .is_some(),
374 vc_external_init_secret.is_some(),
375 )?;
376
377 let (commit_secret, new_keypairs, new_leaf_keypair_option, update_path_leaf_node) =
379 if let Some(path) = commit.path.clone() {
380 diff.apply_received_update_path(
383 provider.crypto(),
384 ciphersuite,
385 sender_index,
386 &path,
387 )?;
388
389 diff.update_group_context(
391 provider.crypto(),
392 apply_proposals_values.extensions.clone(),
393 )?;
394
395 if apply_proposals_values.self_removed && !is_sibling_resync {
401 let staged_diff = diff.into_staged_diff(provider.crypto(), ciphersuite)?;
403 let staged_state = PublicStagedCommitState::new(
404 staged_diff,
405 commit.path.as_ref().map(|path| path.leaf_node().clone()),
406 );
407 let staged_commit = StagedCommit::new(
408 proposal_queue,
409 StagedCommitState::PublicState(Box::new(staged_state)),
410 #[cfg(feature = "virtual-clients-draft")]
411 None,
412 );
413 return Ok(staged_commit);
414 }
415
416 #[cfg(feature = "virtual-clients-draft")]
425 let vc_path: Option<(Vec<EncryptionKeyPair>, CommitSecret)> = if sender_index
426 == self.own_leaf_index()
427 || is_sibling_resync
428 {
429 let operation_secret = vc_material.ok_or(
430 crate::components::vc_derivation_info::VirtualClientsError::MissingOperationTree,
431 )?;
432 Some(self.recreate_path_for_own_commit(
433 &diff,
434 &path,
435 ciphersuite,
436 self.group_id(),
437 provider.crypto(),
438 sender_index,
439 operation_secret,
440 )?)
441 } else {
442 None
443 };
444 #[cfg(not(feature = "virtual-clients-draft"))]
445 let vc_path: Option<(Vec<EncryptionKeyPair>, CommitSecret)> = None;
446
447 let (new_keypairs, commit_secret) = if let Some(pair) = vc_path {
450 pair
451 } else {
452 let decryption_keypairs: Vec<&EncryptionKeyPair> = old_epoch_keypairs
453 .iter()
454 .chain(leaf_node_keypairs.iter())
455 .collect();
456 diff.decrypt_path(
457 provider.crypto(),
458 &decryption_keypairs,
459 self.own_leaf_index(),
460 sender_index,
461 path.nodes(),
462 &apply_proposals_values.exclusion_list(),
463 )?
464 };
465
466 let new_leaf_keypair_option = if is_sibling_resync {
472 None
473 } else if let Some(leaf) = diff.leaf(self.own_leaf_index()) {
474 leaf_node_keypairs.into_iter().find_map(|keypair| {
475 if leaf.encryption_key() == keypair.public_key() {
476 Some(keypair)
477 } else {
478 None
479 }
480 })
481 } else {
482 debug_assert!(false);
484 None
485 };
486
487 let update_path_leaf_node = Some(path.leaf_node().clone());
491 debug_assert_eq!(diff.leaf(sender_index), path.leaf_node().into());
492
493 (
494 commit_secret,
495 new_keypairs,
496 new_leaf_keypair_option,
497 update_path_leaf_node,
498 )
499 } else {
500 if apply_proposals_values.path_required {
501 return Err(StageCommitError::RequiredPathNotFound);
503 }
504
505 diff.update_group_context(
507 provider.crypto(),
508 apply_proposals_values.extensions.clone(),
509 )?;
510
511 (CommitSecret::zero_secret(ciphersuite), vec![], None, None)
512 };
513
514 diff.update_confirmed_transcript_hash(provider.crypto(), mls_content)?;
516
517 let received_confirmation_tag = mls_content
518 .confirmation_tag()
519 .ok_or(StageCommitError::ConfirmationTagMissing)?;
520
521 let serialized_provisional_group_context = diff
522 .group_context()
523 .tls_serialize_detached()
524 .map_err(LibraryError::missing_bound_check)?;
525
526 #[cfg(feature = "virtual-clients-draft")]
527 let provisional_own_leaf_index = if is_sibling_resync {
528 sender_index
529 } else {
530 self.own_leaf_index()
531 };
532 #[cfg(not(feature = "virtual-clients-draft"))]
533 let provisional_own_leaf_index = self.own_leaf_index();
534
535 let EpochSecretsResult {
536 epoch_secrets,
537 #[cfg(feature = "extensions-draft")]
538 application_exporter,
539 } = self.derive_epoch_secrets(
540 provider,
541 apply_proposals_values,
542 self.group_epoch_secrets(),
543 commit_secret,
544 &serialized_provisional_group_context,
545 #[cfg(feature = "virtual-clients-draft")]
546 vc_external_init_secret.as_ref(),
547 )?;
548 let (provisional_group_secrets, provisional_message_secrets) = epoch_secrets.split_secrets(
549 serialized_provisional_group_context,
550 diff.tree_size(),
551 provisional_own_leaf_index,
552 );
553
554 let own_confirmation_tag = provisional_message_secrets
557 .confirmation_key()
558 .tag(
559 provider.crypto(),
560 self.ciphersuite(),
561 diff.group_context().confirmed_transcript_hash(),
562 )
563 .map_err(LibraryError::unexpected_crypto_error)?;
564 if &own_confirmation_tag != received_confirmation_tag {
565 log::error!("Confirmation tag mismatch");
566 log_crypto!(trace, " Got: {:x?}", received_confirmation_tag);
567 log_crypto!(trace, " Expected: {:x?}", own_confirmation_tag);
568 if !crate::skip_validation::is_disabled::confirmation_tag() {
575 return Err(StageCommitError::ConfirmationTagMismatch);
576 }
577 }
578
579 diff.update_interim_transcript_hash(ciphersuite, provider.crypto(), own_confirmation_tag)?;
580
581 let staged_diff = diff.into_staged_diff(provider.crypto(), ciphersuite)?;
582 #[cfg(feature = "extensions-draft")]
583 let application_export_tree = ApplicationExportTree::new(application_exporter);
584 #[cfg(feature = "virtual-clients-draft")]
585 let new_own_leaf_index = is_sibling_resync.then_some(provisional_own_leaf_index);
586 let staged_commit_state =
587 StagedCommitState::GroupMember(Box::new(MemberStagedCommitState::new(
588 provisional_group_secrets,
589 provisional_message_secrets,
590 staged_diff,
591 new_keypairs,
592 new_leaf_keypair_option,
593 update_path_leaf_node,
594 #[cfg(feature = "extensions-draft")]
595 application_export_tree,
596 #[cfg(feature = "virtual-clients-draft")]
597 new_own_leaf_index,
598 )));
599 let staged_commit = StagedCommit::new(
600 proposal_queue,
601 staged_commit_state,
602 #[cfg(feature = "virtual-clients-draft")]
603 vc_emulation_epoch_id,
604 );
605
606 Ok(staged_commit)
607 }
608
609 #[cfg(feature = "virtual-clients-draft")]
631 #[expect(clippy::too_many_arguments)]
632 fn recreate_path_for_own_commit(
633 &self,
634 diff: &PublicGroupDiff,
635 path: &crate::treesync::treekem::UpdatePath,
636 group_ciphersuite: openmls_traits::types::Ciphersuite,
637 group_id: &crate::prelude::GroupId,
638 crypto: &impl OpenMlsCrypto,
639 sender_index: LeafNodeIndex,
640 operation_secret: crate::components::vc_derivation_info::OperationSecret,
641 ) -> Result<(Vec<EncryptionKeyPair>, CommitSecret), StageCommitError> {
642 use crate::components::vc_derivation_info::VirtualClientsError;
643
644 let target_operation_secret =
645 operation_secret.derive_target_operation_secret(crypto, group_ciphersuite, group_id)?;
646
647 let path_secret = target_operation_secret
648 .derive_path_generation_secret(crypto, group_ciphersuite)?
649 .into();
650 let (encryption_key_pairs, commit_secret) =
651 diff.recreate_path_from_path_secret(crypto, path_secret, sender_index, path.nodes())?;
652
653 let leaf_keypair = target_operation_secret
656 .derive_encryption_key_secret(crypto, group_ciphersuite)?
657 .generate_encryption_key_pair(crypto, group_ciphersuite)?;
658 drop(target_operation_secret);
659 if leaf_keypair.public_key() != path.leaf_node().encryption_key() {
660 return Err(VirtualClientsError::EncryptionKeyMismatch.into());
661 }
662
663 let mut keypairs = Vec::with_capacity(1 + encryption_key_pairs.len());
667 keypairs.push(leaf_keypair);
668 keypairs.extend(encryption_key_pairs);
669 Ok((keypairs, commit_secret))
670 }
671
672 pub(crate) fn merge_commit<Provider: OpenMlsProvider>(
678 &mut self,
679 provider: &Provider,
680 staged_commit: StagedCommit,
681 ) -> Result<(), MergeCommitError<Provider::StorageError>> {
682 let old_epoch_keypairs = self
685 .read_epoch_keypairs(provider.storage())
686 .map_err(MergeCommitError::StorageError)?;
687 match staged_commit.state {
688 StagedCommitState::PublicState(staged_state) => {
689 self.public_group
690 .merge_diff(staged_state.into_staged_diff());
691 self.store(provider.storage())
692 .map_err(MergeCommitError::StorageError)?;
693 Ok(())
694 }
695 StagedCommitState::GroupMember(state) => {
696 let past_epoch = self.context().epoch();
698 let leaves = self.public_group().members().collect();
700 self.group_epoch_secrets = state.group_epoch_secrets;
703
704 let old_message_secrets = self
706 .message_secrets_store
707 .replace_current_message_secrets(state.message_secrets);
708 self.message_secrets_store.add_past_epoch_tree(
709 past_epoch,
710 old_message_secrets,
711 leaves,
712 );
713
714 #[cfg(feature = "extensions-draft")]
716 {
717 if let Some(application_export_tree) = state.application_export_tree {
721 use openmls_traits::storage::StorageProvider as _;
724 provider
725 .storage()
726 .write_application_export_tree(
727 self.group_id(),
728 &application_export_tree,
729 )
730 .map_err(MergeCommitError::StorageError)?;
731
732 self.application_export_tree = Some(application_export_tree);
733 }
734 }
735
736 self.public_group.merge_diff(state.staged_diff);
737
738 #[cfg(feature = "virtual-clients-draft")]
739 let previous_own_leaf_index = self.own_leaf_index;
740
741 #[cfg(feature = "virtual-clients-draft")]
746 if let Some(new_idx) = state.new_own_leaf_index {
747 self.own_leaf_index = new_idx;
748 }
749
750 let leaf_keypair = if let Some(keypair) = &state.new_leaf_keypair_option {
751 vec![keypair.clone()]
752 } else {
753 vec![]
754 };
755
756 let new_owned_encryption_keys = self
758 .public_group()
759 .owned_encryption_keys(self.own_leaf_index());
760 let epoch_keypairs: Vec<EncryptionKeyPair> = old_epoch_keypairs
762 .into_iter()
763 .chain(state.new_keypairs)
764 .chain(leaf_keypair)
765 .filter(|keypair| new_owned_encryption_keys.contains(keypair.public_key()))
766 .collect();
767
768 debug_assert_eq!(new_owned_encryption_keys.len(), epoch_keypairs.len());
770 if new_owned_encryption_keys.len() != epoch_keypairs.len() {
771 return Err(LibraryError::custom(
772 "We should have all the private key material we need.",
773 )
774 .into());
775 }
776
777 let storage = provider.storage();
779 let group_id = self.group_id();
780
781 self.public_group
782 .store(storage)
783 .map_err(MergeCommitError::StorageError)?;
784 storage
785 .write_own_leaf_index(group_id, &self.own_leaf_index)
786 .map_err(MergeCommitError::StorageError)?;
787 storage
788 .write_group_epoch_secrets(group_id, &self.group_epoch_secrets)
789 .map_err(MergeCommitError::StorageError)?;
790 storage
791 .write_message_secrets(group_id, &self.message_secrets_store)
792 .map_err(MergeCommitError::StorageError)?;
793
794 self.store_epoch_keypairs(storage, epoch_keypairs.as_slice())
796 .map_err(MergeCommitError::StorageError)?;
797
798 self.delete_previous_epoch_keypairs(
800 storage,
801 #[cfg(feature = "virtual-clients-draft")]
802 previous_own_leaf_index,
803 )
804 .map_err(MergeCommitError::StorageError)?;
805 if let Some(keypair) = state.new_leaf_keypair_option {
806 keypair
807 .delete(storage)
808 .map_err(MergeCommitError::StorageError)?;
809 }
810
811 storage
813 .clear_proposal_queue::<GroupId, ProposalRef>(group_id)
814 .map_err(MergeCommitError::StorageError)?;
815 self.proposal_store_mut().empty();
816
817 Ok(())
818 }
819 }
820 }
821}
822
823#[derive(Debug, Serialize, Deserialize)]
824#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
825pub(crate) enum StagedCommitState {
826 PublicState(Box<PublicStagedCommitState>),
827 GroupMember(Box<MemberStagedCommitState>),
829}
830
831#[derive(Debug, Serialize, Deserialize)]
833#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
834pub struct StagedCommit {
835 pub staged_proposal_queue: ProposalQueue,
837 pub(super) state: StagedCommitState,
839 #[cfg(feature = "virtual-clients-draft")]
842 #[serde(default)]
843 pub(super) vc_emulation_epoch_id: Option<crate::components::vc_derivation_info::EpochId>,
844}
845
846impl StagedCommit {
847 pub(crate) fn new(
850 staged_proposal_queue: ProposalQueue,
851 state: StagedCommitState,
852 #[cfg(feature = "virtual-clients-draft")] vc_emulation_epoch_id: Option<
853 crate::components::vc_derivation_info::EpochId,
854 >,
855 ) -> Self {
856 StagedCommit {
857 staged_proposal_queue,
858 state,
859 #[cfg(feature = "virtual-clients-draft")]
860 vc_emulation_epoch_id,
861 }
862 }
863
864 pub fn epoch(&self) -> GroupEpoch {
866 self.group_context().epoch()
867 }
868
869 pub fn export_ratchet_tree(
871 &self,
872 crypto: &impl OpenMlsCrypto,
873 original_tree: RatchetTree,
874 ) -> Result<Option<RatchetTree>, TreeSyncFromNodesError> {
875 match &self.state {
876 StagedCommitState::PublicState(_public_staged_commit_state) => Ok(None),
877 StagedCommitState::GroupMember(member_staged_commit_state) => Ok(Some(
878 member_staged_commit_state.staged_diff.export_ratchet_tree(
879 crypto,
880 self.group_context().ciphersuite(),
881 original_tree,
882 )?,
883 )),
884 }
885 }
886
887 pub fn add_proposals(&self) -> impl Iterator<Item = QueuedAddProposal<'_>> {
889 self.staged_proposal_queue.add_proposals()
890 }
891
892 pub fn remove_proposals(&self) -> impl Iterator<Item = QueuedRemoveProposal<'_>> {
894 self.staged_proposal_queue.remove_proposals()
895 }
896
897 pub fn update_proposals(&self) -> impl Iterator<Item = QueuedUpdateProposal<'_>> {
899 self.staged_proposal_queue.update_proposals()
900 }
901
902 pub fn psk_proposals(&self) -> impl Iterator<Item = QueuedPskProposal<'_>> {
904 self.staged_proposal_queue.psk_proposals()
905 }
906
907 #[cfg(feature = "extensions-draft")]
908 pub fn queued_app_ephemeral_proposals(
911 &self,
912 ) -> impl Iterator<Item = QueuedAppEphemeralProposal<'_>> {
913 self.staged_proposal_queue.app_ephemeral_proposals()
914 }
915 #[cfg(feature = "extensions-draft")]
917 pub fn app_data_update_proposals(
920 &self,
921 ) -> impl Iterator<Item = QueuedAppDataUpdateProposal<'_>> {
922 self.staged_proposal_queue.app_data_update_proposals()
923 }
924
925 pub fn queued_proposals(&self) -> impl Iterator<Item = &QueuedProposal> {
927 self.staged_proposal_queue.queued_proposals()
928 }
929
930 pub fn update_path_leaf_node(&self) -> Option<&LeafNode> {
932 match self.state {
933 StagedCommitState::PublicState(ref public_state) => {
934 public_state.update_path_leaf_node()
935 }
936 StagedCommitState::GroupMember(ref group_member_state) => {
937 group_member_state.update_path_leaf_node.as_ref()
938 }
939 }
940 }
941
942 pub fn credentials_to_verify(&self) -> impl Iterator<Item = &Credential> {
944 let update_path_leaf_node_cred = if let Some(node) = self.update_path_leaf_node() {
945 vec![node.credential()]
946 } else {
947 vec![]
948 };
949
950 update_path_leaf_node_cred
951 .into_iter()
952 .chain(
953 self.queued_proposals()
954 .flat_map(|proposal: &QueuedProposal| match proposal.proposal() {
955 Proposal::Update(update_proposal) => {
956 vec![update_proposal.leaf_node().credential()].into_iter()
957 }
958 Proposal::Add(add_proposal) => {
959 vec![add_proposal.key_package().leaf_node().credential()].into_iter()
960 }
961 Proposal::GroupContextExtensions(gce_proposal) => gce_proposal
962 .extensions()
963 .iter()
964 .flat_map(|extension| {
965 match extension {
966 Extension::ExternalSenders(external_senders) => {
967 external_senders
968 .iter()
969 .map(|external_sender| external_sender.credential())
970 .collect()
971 }
972 _ => vec![],
973 }
974 .into_iter()
975 })
976 .collect::<Vec<_>>()
983 .into_iter(),
984 _ => vec![].into_iter(),
985 }),
986 )
987 }
988
989 pub fn self_removed(&self) -> bool {
998 matches!(self.state, StagedCommitState::PublicState(_))
999 }
1000
1001 pub fn group_context(&self) -> &GroupContext {
1003 match self.state {
1004 StagedCommitState::PublicState(ref ps) => ps.staged_diff().group_context(),
1005 StagedCommitState::GroupMember(ref gm) => gm.group_context(),
1006 }
1007 }
1008 pub(crate) fn into_state(self) -> StagedCommitState {
1010 self.state
1011 }
1012
1013 pub fn epoch_authenticator(&self) -> Option<&EpochAuthenticator> {
1017 if let StagedCommitState::GroupMember(ref gm) = self.state {
1018 Some(gm.group_epoch_secrets.epoch_authenticator())
1019 } else {
1020 None
1021 }
1022 }
1023
1024 pub fn resumption_psk_secret(&self) -> Option<&ResumptionPskSecret> {
1028 if let StagedCommitState::GroupMember(ref gm) = self.state {
1029 Some(gm.group_epoch_secrets.resumption_psk())
1030 } else {
1031 None
1032 }
1033 }
1034
1035 #[cfg(feature = "extensions-draft")]
1043 pub fn safe_export_secret(
1044 &mut self,
1045 crypto: &impl OpenMlsCrypto,
1046 component_id: ComponentId,
1047 ) -> Result<Vec<u8>, StagedSafeExportSecretError> {
1048 let ciphersuite = self.group_context().ciphersuite();
1049 let StagedCommitState::GroupMember(ref mut staged_commit) = self.state else {
1050 return Err(StagedSafeExportSecretError::NotGroupMember);
1051 };
1052 let Some(application_export_tree) = staged_commit.application_export_tree.as_mut() else {
1053 return Err(StagedSafeExportSecretError::Unsupported);
1054 };
1055 let secret =
1056 application_export_tree.safe_export_secret(crypto, ciphersuite, component_id)?;
1057 Ok(secret.as_slice().to_vec())
1058 }
1059
1060 pub fn export_secret<CryptoProvider: OpenMlsCrypto>(
1068 &self,
1069 crypto: &CryptoProvider,
1070 label: &str,
1071 context: &[u8],
1072 key_length: usize,
1073 ) -> Result<Vec<u8>, ExportSecretError> {
1074 if key_length > u16::MAX as usize {
1075 log::error!("Got a key that is larger than u16::MAX");
1076 return Err(ExportSecretError::KeyLengthTooLong);
1077 }
1078
1079 match &self.state {
1080 StagedCommitState::PublicState(_public_staged_commit_state) => Err(
1081 ExportSecretError::GroupStateError(MlsGroupStateError::UseAfterEviction),
1082 ),
1083 StagedCommitState::GroupMember(member_staged_commit_state) => {
1084 Ok(member_staged_commit_state
1085 .group_epoch_secrets
1086 .exporter_secret()
1087 .derive_exported_secret(
1088 self.group_context().ciphersuite(),
1089 crypto,
1090 label,
1091 context,
1092 key_length,
1093 )
1094 .map_err(LibraryError::unexpected_crypto_error)?)
1095 }
1096 }
1097 }
1098}
1099
1100#[derive(Debug, Serialize, Deserialize)]
1102#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
1103pub(crate) struct MemberStagedCommitState {
1104 group_epoch_secrets: GroupEpochSecrets,
1105 message_secrets: MessageSecrets,
1106 staged_diff: StagedPublicGroupDiff,
1107 new_keypairs: Vec<EncryptionKeyPair>,
1108 new_leaf_keypair_option: Option<EncryptionKeyPair>,
1109 update_path_leaf_node: Option<LeafNode>,
1110 #[cfg(feature = "extensions-draft")]
1111 #[serde(default)]
1112 application_export_tree: Option<ApplicationExportTree>,
1115 #[cfg(feature = "virtual-clients-draft")]
1120 #[serde(default)]
1121 new_own_leaf_index: Option<LeafNodeIndex>,
1122}
1123
1124impl MemberStagedCommitState {
1125 #[allow(clippy::too_many_arguments)]
1126 pub(crate) fn new(
1127 group_epoch_secrets: GroupEpochSecrets,
1128 message_secrets: MessageSecrets,
1129 staged_diff: StagedPublicGroupDiff,
1130 new_keypairs: Vec<EncryptionKeyPair>,
1131 new_leaf_keypair_option: Option<EncryptionKeyPair>,
1132 update_path_leaf_node: Option<LeafNode>,
1133 #[cfg(feature = "extensions-draft")] application_export_tree: ApplicationExportTree,
1134 #[cfg(feature = "virtual-clients-draft")] new_own_leaf_index: Option<LeafNodeIndex>,
1135 ) -> Self {
1136 Self {
1137 group_epoch_secrets,
1138 message_secrets,
1139 staged_diff,
1140 new_keypairs,
1141 new_leaf_keypair_option,
1142 update_path_leaf_node,
1143 #[cfg(feature = "extensions-draft")]
1144 application_export_tree: Some(application_export_tree),
1145 #[cfg(feature = "virtual-clients-draft")]
1146 new_own_leaf_index,
1147 }
1148 }
1149
1150 pub(crate) fn group_context(&self) -> &GroupContext {
1152 self.staged_diff.group_context()
1153 }
1154}
1155
1156#[cfg(all(test, feature = "virtual-clients-draft"))]
1157mod tests {
1158 use super::validate_vc_external_init_secret;
1159 use crate::{
1160 components::vc_derivation_info::VirtualClientsError, group::errors::StageCommitError,
1161 };
1162
1163 #[test]
1168 fn external_init_secret_presence_is_validated() {
1169 let malformed: Result<(), StageCommitError> =
1170 Err(VirtualClientsError::DerivationInfoMalformed.into());
1171
1172 assert_eq!(
1174 validate_vc_external_init_secret(true, true, false),
1175 malformed
1176 );
1177 assert_eq!(
1179 validate_vc_external_init_secret(false, false, true),
1180 malformed
1181 );
1182 assert_eq!(
1183 validate_vc_external_init_secret(true, false, true),
1184 malformed
1185 );
1186
1187 assert_eq!(validate_vc_external_init_secret(true, true, true), Ok(()));
1190 assert_eq!(validate_vc_external_init_secret(false, true, false), Ok(()));
1191 assert_eq!(
1192 validate_vc_external_init_secret(false, false, false),
1193 Ok(())
1194 );
1195 }
1196}