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 provider.crypto(),
437 sender_index,
438 operation_secret,
439 )?)
440 } else {
441 None
442 };
443 #[cfg(not(feature = "virtual-clients-draft"))]
444 let vc_path: Option<(Vec<EncryptionKeyPair>, CommitSecret)> = None;
445
446 let (new_keypairs, commit_secret) = if let Some(pair) = vc_path {
449 pair
450 } else {
451 let decryption_keypairs: Vec<&EncryptionKeyPair> = old_epoch_keypairs
452 .iter()
453 .chain(leaf_node_keypairs.iter())
454 .collect();
455 diff.decrypt_path(
456 provider.crypto(),
457 &decryption_keypairs,
458 self.own_leaf_index(),
459 sender_index,
460 path.nodes(),
461 &apply_proposals_values.exclusion_list(),
462 )?
463 };
464
465 let new_leaf_keypair_option = if is_sibling_resync {
471 None
472 } else if let Some(leaf) = diff.leaf(self.own_leaf_index()) {
473 leaf_node_keypairs.into_iter().find_map(|keypair| {
474 if leaf.encryption_key() == keypair.public_key() {
475 Some(keypair)
476 } else {
477 None
478 }
479 })
480 } else {
481 debug_assert!(false);
483 None
484 };
485
486 let update_path_leaf_node = Some(path.leaf_node().clone());
490 debug_assert_eq!(diff.leaf(sender_index), path.leaf_node().into());
491
492 (
493 commit_secret,
494 new_keypairs,
495 new_leaf_keypair_option,
496 update_path_leaf_node,
497 )
498 } else {
499 if apply_proposals_values.path_required {
500 return Err(StageCommitError::RequiredPathNotFound);
502 }
503
504 diff.update_group_context(
506 provider.crypto(),
507 apply_proposals_values.extensions.clone(),
508 )?;
509
510 (CommitSecret::zero_secret(ciphersuite), vec![], None, None)
511 };
512
513 diff.update_confirmed_transcript_hash(provider.crypto(), mls_content)?;
515
516 let received_confirmation_tag = mls_content
517 .confirmation_tag()
518 .ok_or(StageCommitError::ConfirmationTagMissing)?;
519
520 let serialized_provisional_group_context = diff
521 .group_context()
522 .tls_serialize_detached()
523 .map_err(LibraryError::missing_bound_check)?;
524
525 #[cfg(feature = "virtual-clients-draft")]
526 let provisional_own_leaf_index = if is_sibling_resync {
527 sender_index
528 } else {
529 self.own_leaf_index()
530 };
531 #[cfg(not(feature = "virtual-clients-draft"))]
532 let provisional_own_leaf_index = self.own_leaf_index();
533
534 let EpochSecretsResult {
535 epoch_secrets,
536 #[cfg(feature = "extensions-draft")]
537 application_exporter,
538 } = self.derive_epoch_secrets(
539 provider,
540 apply_proposals_values,
541 self.group_epoch_secrets(),
542 commit_secret,
543 &serialized_provisional_group_context,
544 #[cfg(feature = "virtual-clients-draft")]
545 vc_external_init_secret.as_ref(),
546 )?;
547 let (provisional_group_secrets, provisional_message_secrets) = epoch_secrets.split_secrets(
548 serialized_provisional_group_context,
549 diff.tree_size(),
550 provisional_own_leaf_index,
551 );
552
553 let own_confirmation_tag = provisional_message_secrets
556 .confirmation_key()
557 .tag(
558 provider.crypto(),
559 self.ciphersuite(),
560 diff.group_context().confirmed_transcript_hash(),
561 )
562 .map_err(LibraryError::unexpected_crypto_error)?;
563 if &own_confirmation_tag != received_confirmation_tag {
564 log::error!("Confirmation tag mismatch");
565 log_crypto!(trace, " Got: {:x?}", received_confirmation_tag);
566 log_crypto!(trace, " Expected: {:x?}", own_confirmation_tag);
567 if !crate::skip_validation::is_disabled::confirmation_tag() {
574 return Err(StageCommitError::ConfirmationTagMismatch);
575 }
576 }
577
578 diff.update_interim_transcript_hash(ciphersuite, provider.crypto(), own_confirmation_tag)?;
579
580 let staged_diff = diff.into_staged_diff(provider.crypto(), ciphersuite)?;
581 #[cfg(feature = "extensions-draft")]
582 let application_export_tree = ApplicationExportTree::new(application_exporter);
583 #[cfg(feature = "virtual-clients-draft")]
584 let new_own_leaf_index = is_sibling_resync.then_some(provisional_own_leaf_index);
585 let staged_commit_state =
586 StagedCommitState::GroupMember(Box::new(MemberStagedCommitState::new(
587 provisional_group_secrets,
588 provisional_message_secrets,
589 staged_diff,
590 new_keypairs,
591 new_leaf_keypair_option,
592 update_path_leaf_node,
593 #[cfg(feature = "extensions-draft")]
594 application_export_tree,
595 #[cfg(feature = "virtual-clients-draft")]
596 new_own_leaf_index,
597 )));
598 let staged_commit = StagedCommit::new(
599 proposal_queue,
600 staged_commit_state,
601 #[cfg(feature = "virtual-clients-draft")]
602 vc_emulation_epoch_id,
603 );
604
605 Ok(staged_commit)
606 }
607
608 #[cfg(feature = "virtual-clients-draft")]
630 fn recreate_path_for_own_commit(
631 &self,
632 diff: &PublicGroupDiff,
633 path: &crate::treesync::treekem::UpdatePath,
634 ciphersuite: openmls_traits::types::Ciphersuite,
635 crypto: &impl OpenMlsCrypto,
636 sender_index: LeafNodeIndex,
637 operation_secret: crate::components::vc_derivation_info::OperationSecret,
638 ) -> Result<(Vec<EncryptionKeyPair>, CommitSecret), StageCommitError> {
639 use crate::components::vc_derivation_info::VirtualClientsError;
640
641 let path_secret = operation_secret
642 .derive_path_generation_secret(crypto, ciphersuite)?
643 .into();
644 let (encryption_key_pairs, commit_secret) =
645 diff.recreate_path_from_path_secret(crypto, path_secret, sender_index, path.nodes())?;
646
647 let leaf_keypair = operation_secret
650 .derive_encryption_key_secret(crypto, ciphersuite)?
651 .generate_encryption_key_pair(crypto, ciphersuite)?;
652 if leaf_keypair.public_key() != path.leaf_node().encryption_key() {
653 return Err(VirtualClientsError::EncryptionKeyMismatch.into());
654 }
655
656 let mut keypairs = Vec::with_capacity(1 + encryption_key_pairs.len());
660 keypairs.push(leaf_keypair);
661 keypairs.extend(encryption_key_pairs);
662 Ok((keypairs, commit_secret))
663 }
664
665 pub(crate) fn merge_commit<Provider: OpenMlsProvider>(
671 &mut self,
672 provider: &Provider,
673 staged_commit: StagedCommit,
674 ) -> Result<(), MergeCommitError<Provider::StorageError>> {
675 let old_epoch_keypairs = self
678 .read_epoch_keypairs(provider.storage())
679 .map_err(MergeCommitError::StorageError)?;
680 match staged_commit.state {
681 StagedCommitState::PublicState(staged_state) => {
682 self.public_group
683 .merge_diff(staged_state.into_staged_diff());
684 self.store(provider.storage())
685 .map_err(MergeCommitError::StorageError)?;
686 Ok(())
687 }
688 StagedCommitState::GroupMember(state) => {
689 let past_epoch = self.context().epoch();
691 let leaves = self.public_group().members().collect();
693 self.group_epoch_secrets = state.group_epoch_secrets;
696
697 let old_message_secrets = self
699 .message_secrets_store
700 .replace_current_message_secrets(state.message_secrets);
701 self.message_secrets_store.add_past_epoch_tree(
702 past_epoch,
703 old_message_secrets,
704 leaves,
705 );
706
707 #[cfg(feature = "extensions-draft")]
709 {
710 if let Some(application_export_tree) = state.application_export_tree {
714 use openmls_traits::storage::StorageProvider as _;
717 provider
718 .storage()
719 .write_application_export_tree(
720 self.group_id(),
721 &application_export_tree,
722 )
723 .map_err(MergeCommitError::StorageError)?;
724
725 self.application_export_tree = Some(application_export_tree);
726 }
727 }
728
729 self.public_group.merge_diff(state.staged_diff);
730
731 #[cfg(feature = "virtual-clients-draft")]
732 let previous_own_leaf_index = self.own_leaf_index;
733
734 #[cfg(feature = "virtual-clients-draft")]
739 if let Some(new_idx) = state.new_own_leaf_index {
740 self.own_leaf_index = new_idx;
741 }
742
743 let leaf_keypair = if let Some(keypair) = &state.new_leaf_keypair_option {
744 vec![keypair.clone()]
745 } else {
746 vec![]
747 };
748
749 let new_owned_encryption_keys = self
751 .public_group()
752 .owned_encryption_keys(self.own_leaf_index());
753 let epoch_keypairs: Vec<EncryptionKeyPair> = old_epoch_keypairs
755 .into_iter()
756 .chain(state.new_keypairs)
757 .chain(leaf_keypair)
758 .filter(|keypair| new_owned_encryption_keys.contains(keypair.public_key()))
759 .collect();
760
761 debug_assert_eq!(new_owned_encryption_keys.len(), epoch_keypairs.len());
763 if new_owned_encryption_keys.len() != epoch_keypairs.len() {
764 return Err(LibraryError::custom(
765 "We should have all the private key material we need.",
766 )
767 .into());
768 }
769
770 let storage = provider.storage();
772 let group_id = self.group_id();
773
774 self.public_group
775 .store(storage)
776 .map_err(MergeCommitError::StorageError)?;
777 storage
778 .write_own_leaf_index(group_id, &self.own_leaf_index)
779 .map_err(MergeCommitError::StorageError)?;
780 storage
781 .write_group_epoch_secrets(group_id, &self.group_epoch_secrets)
782 .map_err(MergeCommitError::StorageError)?;
783 storage
784 .write_message_secrets(group_id, &self.message_secrets_store)
785 .map_err(MergeCommitError::StorageError)?;
786
787 self.store_epoch_keypairs(storage, epoch_keypairs.as_slice())
789 .map_err(MergeCommitError::StorageError)?;
790
791 self.delete_previous_epoch_keypairs(
793 storage,
794 #[cfg(feature = "virtual-clients-draft")]
795 previous_own_leaf_index,
796 )
797 .map_err(MergeCommitError::StorageError)?;
798 if let Some(keypair) = state.new_leaf_keypair_option {
799 keypair
800 .delete(storage)
801 .map_err(MergeCommitError::StorageError)?;
802 }
803
804 storage
806 .clear_proposal_queue::<GroupId, ProposalRef>(group_id)
807 .map_err(MergeCommitError::StorageError)?;
808 self.proposal_store_mut().empty();
809
810 Ok(())
811 }
812 }
813 }
814}
815
816#[derive(Debug, Serialize, Deserialize)]
817#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
818pub(crate) enum StagedCommitState {
819 PublicState(Box<PublicStagedCommitState>),
820 GroupMember(Box<MemberStagedCommitState>),
822}
823
824#[derive(Debug, Serialize, Deserialize)]
826#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
827pub struct StagedCommit {
828 pub staged_proposal_queue: ProposalQueue,
830 pub(super) state: StagedCommitState,
832 #[cfg(feature = "virtual-clients-draft")]
835 #[serde(default)]
836 pub(super) vc_emulation_epoch_id: Option<crate::components::vc_derivation_info::EpochId>,
837}
838
839impl StagedCommit {
840 pub(crate) fn new(
843 staged_proposal_queue: ProposalQueue,
844 state: StagedCommitState,
845 #[cfg(feature = "virtual-clients-draft")] vc_emulation_epoch_id: Option<
846 crate::components::vc_derivation_info::EpochId,
847 >,
848 ) -> Self {
849 StagedCommit {
850 staged_proposal_queue,
851 state,
852 #[cfg(feature = "virtual-clients-draft")]
853 vc_emulation_epoch_id,
854 }
855 }
856
857 pub fn epoch(&self) -> GroupEpoch {
859 self.group_context().epoch()
860 }
861
862 pub fn export_ratchet_tree(
864 &self,
865 crypto: &impl OpenMlsCrypto,
866 original_tree: RatchetTree,
867 ) -> Result<Option<RatchetTree>, TreeSyncFromNodesError> {
868 match &self.state {
869 StagedCommitState::PublicState(_public_staged_commit_state) => Ok(None),
870 StagedCommitState::GroupMember(member_staged_commit_state) => Ok(Some(
871 member_staged_commit_state.staged_diff.export_ratchet_tree(
872 crypto,
873 self.group_context().ciphersuite(),
874 original_tree,
875 )?,
876 )),
877 }
878 }
879
880 pub fn add_proposals(&self) -> impl Iterator<Item = QueuedAddProposal<'_>> {
882 self.staged_proposal_queue.add_proposals()
883 }
884
885 pub fn remove_proposals(&self) -> impl Iterator<Item = QueuedRemoveProposal<'_>> {
887 self.staged_proposal_queue.remove_proposals()
888 }
889
890 pub fn update_proposals(&self) -> impl Iterator<Item = QueuedUpdateProposal<'_>> {
892 self.staged_proposal_queue.update_proposals()
893 }
894
895 pub fn psk_proposals(&self) -> impl Iterator<Item = QueuedPskProposal<'_>> {
897 self.staged_proposal_queue.psk_proposals()
898 }
899
900 #[cfg(feature = "extensions-draft")]
901 pub fn queued_app_ephemeral_proposals(
904 &self,
905 ) -> impl Iterator<Item = QueuedAppEphemeralProposal<'_>> {
906 self.staged_proposal_queue.app_ephemeral_proposals()
907 }
908 #[cfg(feature = "extensions-draft")]
910 pub fn app_data_update_proposals(
913 &self,
914 ) -> impl Iterator<Item = QueuedAppDataUpdateProposal<'_>> {
915 self.staged_proposal_queue.app_data_update_proposals()
916 }
917
918 pub fn queued_proposals(&self) -> impl Iterator<Item = &QueuedProposal> {
920 self.staged_proposal_queue.queued_proposals()
921 }
922
923 pub fn update_path_leaf_node(&self) -> Option<&LeafNode> {
925 match self.state {
926 StagedCommitState::PublicState(ref public_state) => {
927 public_state.update_path_leaf_node()
928 }
929 StagedCommitState::GroupMember(ref group_member_state) => {
930 group_member_state.update_path_leaf_node.as_ref()
931 }
932 }
933 }
934
935 pub fn credentials_to_verify(&self) -> impl Iterator<Item = &Credential> {
937 let update_path_leaf_node_cred = if let Some(node) = self.update_path_leaf_node() {
938 vec![node.credential()]
939 } else {
940 vec![]
941 };
942
943 update_path_leaf_node_cred
944 .into_iter()
945 .chain(
946 self.queued_proposals()
947 .flat_map(|proposal: &QueuedProposal| match proposal.proposal() {
948 Proposal::Update(update_proposal) => {
949 vec![update_proposal.leaf_node().credential()].into_iter()
950 }
951 Proposal::Add(add_proposal) => {
952 vec![add_proposal.key_package().leaf_node().credential()].into_iter()
953 }
954 Proposal::GroupContextExtensions(gce_proposal) => gce_proposal
955 .extensions()
956 .iter()
957 .flat_map(|extension| {
958 match extension {
959 Extension::ExternalSenders(external_senders) => {
960 external_senders
961 .iter()
962 .map(|external_sender| external_sender.credential())
963 .collect()
964 }
965 _ => vec![],
966 }
967 .into_iter()
968 })
969 .collect::<Vec<_>>()
976 .into_iter(),
977 _ => vec![].into_iter(),
978 }),
979 )
980 }
981
982 pub fn self_removed(&self) -> bool {
991 matches!(self.state, StagedCommitState::PublicState(_))
992 }
993
994 pub fn group_context(&self) -> &GroupContext {
996 match self.state {
997 StagedCommitState::PublicState(ref ps) => ps.staged_diff().group_context(),
998 StagedCommitState::GroupMember(ref gm) => gm.group_context(),
999 }
1000 }
1001 pub(crate) fn into_state(self) -> StagedCommitState {
1003 self.state
1004 }
1005
1006 pub fn epoch_authenticator(&self) -> Option<&EpochAuthenticator> {
1010 if let StagedCommitState::GroupMember(ref gm) = self.state {
1011 Some(gm.group_epoch_secrets.epoch_authenticator())
1012 } else {
1013 None
1014 }
1015 }
1016
1017 pub fn resumption_psk_secret(&self) -> Option<&ResumptionPskSecret> {
1021 if let StagedCommitState::GroupMember(ref gm) = self.state {
1022 Some(gm.group_epoch_secrets.resumption_psk())
1023 } else {
1024 None
1025 }
1026 }
1027
1028 #[cfg(feature = "extensions-draft")]
1036 pub fn safe_export_secret(
1037 &mut self,
1038 crypto: &impl OpenMlsCrypto,
1039 component_id: ComponentId,
1040 ) -> Result<Vec<u8>, StagedSafeExportSecretError> {
1041 let ciphersuite = self.group_context().ciphersuite();
1042 let StagedCommitState::GroupMember(ref mut staged_commit) = self.state else {
1043 return Err(StagedSafeExportSecretError::NotGroupMember);
1044 };
1045 let Some(application_export_tree) = staged_commit.application_export_tree.as_mut() else {
1046 return Err(StagedSafeExportSecretError::Unsupported);
1047 };
1048 let secret =
1049 application_export_tree.safe_export_secret(crypto, ciphersuite, component_id)?;
1050 Ok(secret.as_slice().to_vec())
1051 }
1052
1053 pub fn export_secret<CryptoProvider: OpenMlsCrypto>(
1061 &self,
1062 crypto: &CryptoProvider,
1063 label: &str,
1064 context: &[u8],
1065 key_length: usize,
1066 ) -> Result<Vec<u8>, ExportSecretError> {
1067 if key_length > u16::MAX as usize {
1068 log::error!("Got a key that is larger than u16::MAX");
1069 return Err(ExportSecretError::KeyLengthTooLong);
1070 }
1071
1072 match &self.state {
1073 StagedCommitState::PublicState(_public_staged_commit_state) => Err(
1074 ExportSecretError::GroupStateError(MlsGroupStateError::UseAfterEviction),
1075 ),
1076 StagedCommitState::GroupMember(member_staged_commit_state) => {
1077 Ok(member_staged_commit_state
1078 .group_epoch_secrets
1079 .exporter_secret()
1080 .derive_exported_secret(
1081 self.group_context().ciphersuite(),
1082 crypto,
1083 label,
1084 context,
1085 key_length,
1086 )
1087 .map_err(LibraryError::unexpected_crypto_error)?)
1088 }
1089 }
1090 }
1091}
1092
1093#[derive(Debug, Serialize, Deserialize)]
1095#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
1096pub(crate) struct MemberStagedCommitState {
1097 group_epoch_secrets: GroupEpochSecrets,
1098 message_secrets: MessageSecrets,
1099 staged_diff: StagedPublicGroupDiff,
1100 new_keypairs: Vec<EncryptionKeyPair>,
1101 new_leaf_keypair_option: Option<EncryptionKeyPair>,
1102 update_path_leaf_node: Option<LeafNode>,
1103 #[cfg(feature = "extensions-draft")]
1104 #[serde(default)]
1105 application_export_tree: Option<ApplicationExportTree>,
1108 #[cfg(feature = "virtual-clients-draft")]
1113 #[serde(default)]
1114 new_own_leaf_index: Option<LeafNodeIndex>,
1115}
1116
1117impl MemberStagedCommitState {
1118 #[allow(clippy::too_many_arguments)]
1119 pub(crate) fn new(
1120 group_epoch_secrets: GroupEpochSecrets,
1121 message_secrets: MessageSecrets,
1122 staged_diff: StagedPublicGroupDiff,
1123 new_keypairs: Vec<EncryptionKeyPair>,
1124 new_leaf_keypair_option: Option<EncryptionKeyPair>,
1125 update_path_leaf_node: Option<LeafNode>,
1126 #[cfg(feature = "extensions-draft")] application_export_tree: ApplicationExportTree,
1127 #[cfg(feature = "virtual-clients-draft")] new_own_leaf_index: Option<LeafNodeIndex>,
1128 ) -> Self {
1129 Self {
1130 group_epoch_secrets,
1131 message_secrets,
1132 staged_diff,
1133 new_keypairs,
1134 new_leaf_keypair_option,
1135 update_path_leaf_node,
1136 #[cfg(feature = "extensions-draft")]
1137 application_export_tree: Some(application_export_tree),
1138 #[cfg(feature = "virtual-clients-draft")]
1139 new_own_leaf_index,
1140 }
1141 }
1142
1143 pub(crate) fn group_context(&self) -> &GroupContext {
1145 self.staged_diff.group_context()
1146 }
1147}
1148
1149#[cfg(all(test, feature = "virtual-clients-draft"))]
1150mod tests {
1151 use super::validate_vc_external_init_secret;
1152 use crate::{
1153 components::vc_derivation_info::VirtualClientsError, group::errors::StageCommitError,
1154 };
1155
1156 #[test]
1161 fn external_init_secret_presence_is_validated() {
1162 let malformed: Result<(), StageCommitError> =
1163 Err(VirtualClientsError::DerivationInfoMalformed.into());
1164
1165 assert_eq!(
1167 validate_vc_external_init_secret(true, true, false),
1168 malformed
1169 );
1170 assert_eq!(
1172 validate_vc_external_init_secret(false, false, true),
1173 malformed
1174 );
1175 assert_eq!(
1176 validate_vc_external_init_secret(true, false, true),
1177 malformed
1178 );
1179
1180 assert_eq!(validate_vc_external_init_secret(true, true, true), Ok(()));
1183 assert_eq!(validate_vc_external_init_secret(false, true, false), Ok(()));
1184 assert_eq!(
1185 validate_vc_external_init_secret(false, false, false),
1186 Ok(())
1187 );
1188 }
1189}