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
12use super::{
13 super::errors::*, load_psks, Credential, Extension, GroupContext, GroupEpochSecrets, GroupId,
14 JoinerSecret, KeySchedule, LeafNode, LibraryError, MessageSecrets, MlsGroup, OpenMlsProvider,
15 Proposal, ProposalQueue, PskSecret, QueuedProposal, Sender,
16};
17use crate::group::diff::PublicGroupDiff;
18use crate::group::GroupEpoch;
19use crate::prelude::{Commit, LeafNodeIndex};
20#[cfg(feature = "extensions-draft-08")]
21use crate::schedule::application_export_tree::ApplicationExportTree;
22
23use crate::treesync::errors::TreeSyncFromNodesError;
24use crate::treesync::RatchetTree;
25use crate::{
26 ciphersuite::{hash_ref::ProposalRef, Secret},
27 framing::mls_auth_content::AuthenticatedContent,
28 group::public_group::{
29 diff::{apply_proposals::ApplyProposalsValues, StagedPublicGroupDiff},
30 staged_commit::PublicStagedCommitState,
31 },
32 schedule::{
33 CommitSecret, EpochAuthenticator, EpochSecretsResult, InitSecret, PreSharedKeyId,
34 ResumptionPskSecret,
35 },
36 treesync::node::encryption_keys::EncryptionKeyPair,
37};
38
39#[cfg(feature = "extensions-draft-08")]
40use super::proposal_store::{QueuedAppDataUpdateProposal, QueuedAppEphemeralProposal};
41#[cfg(feature = "extensions-draft-08")]
42use crate::prelude::processing::AppDataUpdates;
43
44impl MlsGroup {
45 fn derive_epoch_secrets(
46 &self,
47 provider: &impl OpenMlsProvider,
48 apply_proposals_values: ApplyProposalsValues,
49 epoch_secrets: &GroupEpochSecrets,
50 commit_secret: CommitSecret,
51 serialized_provisional_group_context: &[u8],
52 ) -> Result<EpochSecretsResult, StageCommitError> {
53 let joiner_secret = if let Some(ref external_init_proposal) =
56 apply_proposals_values.external_init_proposal_option
57 {
58 let external_priv = epoch_secrets
60 .external_secret()
61 .derive_external_keypair(provider.crypto(), self.ciphersuite())
62 .map_err(LibraryError::unexpected_crypto_error)?
63 .private;
64 let init_secret = InitSecret::from_kem_output(
65 provider.crypto(),
66 self.ciphersuite(),
67 self.version(),
68 &external_priv,
69 external_init_proposal.kem_output(),
70 )?;
71 JoinerSecret::new(
72 provider.crypto(),
73 self.ciphersuite(),
74 commit_secret,
75 &init_secret,
76 serialized_provisional_group_context,
77 )
78 .map_err(LibraryError::unexpected_crypto_error)?
79 } else {
80 JoinerSecret::new(
81 provider.crypto(),
82 self.ciphersuite(),
83 commit_secret,
84 epoch_secrets.init_secret(),
85 serialized_provisional_group_context,
86 )
87 .map_err(LibraryError::unexpected_crypto_error)?
88 };
89
90 let psk_secret = {
93 let psks: Vec<(&PreSharedKeyId, Secret)> = load_psks(
94 provider.storage(),
95 &self.resumption_psk_store,
96 &apply_proposals_values.presharedkeys,
97 )?;
98
99 PskSecret::new(provider.crypto(), self.ciphersuite(), psks)?
100 };
101
102 let mut key_schedule = KeySchedule::init(
104 self.ciphersuite(),
105 provider.crypto(),
106 &joiner_secret,
107 psk_secret,
108 )?;
109
110 key_schedule
111 .add_context(provider.crypto(), serialized_provisional_group_context)
112 .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
113 Ok(key_schedule
114 .epoch_secrets(provider.crypto(), self.ciphersuite())
115 .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?)
116 }
117
118 pub(crate) fn stage_commit(
157 &self,
158 mls_content: &AuthenticatedContent,
159 old_epoch_keypairs: Vec<EncryptionKeyPair>,
160 leaf_node_keypairs: Vec<EncryptionKeyPair>,
161 provider: &impl OpenMlsProvider,
162 ) -> Result<StagedCommit, StageCommitError> {
163 if let Sender::Member(member) = mls_content.sender() {
165 if member == &self.own_leaf_index() {
166 return Err(StageCommitError::OwnCommit);
167 }
168 }
169
170 let (commit, proposal_queue, sender_index) = self
171 .public_group
172 .validate_commit(mls_content, provider.crypto())?;
173
174 let mut diff = self.public_group.empty_diff();
177
178 #[cfg(not(feature = "extensions-draft-08"))]
179 let apply_proposals_values =
180 diff.apply_proposals(&proposal_queue, self.own_leaf_index())?;
181
182 #[cfg(feature = "extensions-draft-08")]
183 let apply_proposals_values = diff.apply_proposals_with_app_data_updates(
184 &proposal_queue,
185 self.own_leaf_index(),
186 None,
187 )?;
188 self.stage_applied_proposal_values(
189 apply_proposals_values,
190 diff,
191 commit,
192 proposal_queue,
193 sender_index,
194 mls_content,
195 old_epoch_keypairs,
196 leaf_node_keypairs,
197 provider,
198 )
199 }
200
201 #[cfg(feature = "extensions-draft-08")]
202 pub(crate) fn stage_commit_with_app_data_updates(
203 &self,
204 mls_content: &AuthenticatedContent,
205 old_epoch_keypairs: Vec<EncryptionKeyPair>,
206 leaf_node_keypairs: Vec<EncryptionKeyPair>,
207 app_data_dict_updates: Option<AppDataUpdates>,
208 provider: &impl OpenMlsProvider,
209 ) -> Result<StagedCommit, StageCommitError> {
210 if let Sender::Member(member) = mls_content.sender() {
212 if member == &self.own_leaf_index() {
213 return Err(StageCommitError::OwnCommit);
214 }
215 }
216
217 let (commit, proposal_queue, sender_index) = self
218 .public_group
219 .validate_commit(mls_content, provider.crypto())?;
220
221 let mut diff = self.public_group.empty_diff();
224
225 let apply_proposals_values = diff.apply_proposals_with_app_data_updates(
226 &proposal_queue,
227 self.own_leaf_index(),
228 app_data_dict_updates,
229 )?;
230
231 self.stage_applied_proposal_values(
232 apply_proposals_values,
233 diff,
234 commit,
235 proposal_queue,
236 sender_index,
237 mls_content,
238 old_epoch_keypairs,
239 leaf_node_keypairs,
240 provider,
241 )
242 }
243
244 #[allow(clippy::too_many_arguments)]
245 fn stage_applied_proposal_values(
246 &self,
247 apply_proposals_values: ApplyProposalsValues,
248 mut diff: PublicGroupDiff,
249 commit: &Commit,
250 proposal_queue: ProposalQueue,
251 sender_index: LeafNodeIndex,
252 mls_content: &AuthenticatedContent,
253 old_epoch_keypairs: Vec<EncryptionKeyPair>,
254 leaf_node_keypairs: Vec<EncryptionKeyPair>,
255 provider: &impl OpenMlsProvider,
256 ) -> Result<StagedCommit, StageCommitError> {
257 let ciphersuite = self.ciphersuite();
258 let (commit_secret, new_keypairs, new_leaf_keypair_option, update_path_leaf_node) =
260 if let Some(path) = commit.path.clone() {
261 diff.apply_received_update_path(
264 provider.crypto(),
265 ciphersuite,
266 sender_index,
267 &path,
268 )?;
269
270 diff.update_group_context(
272 provider.crypto(),
273 apply_proposals_values.extensions.clone(),
274 )?;
275
276 if apply_proposals_values.self_removed {
278 let staged_diff = diff.into_staged_diff(provider.crypto(), ciphersuite)?;
280 let staged_state = PublicStagedCommitState::new(
281 staged_diff,
282 commit.path.as_ref().map(|path| path.leaf_node().clone()),
283 );
284 let staged_commit = StagedCommit::new(
285 proposal_queue,
286 StagedCommitState::PublicState(Box::new(staged_state)),
287 );
288 return Ok(staged_commit);
289 }
290
291 let decryption_keypairs: Vec<&EncryptionKeyPair> = old_epoch_keypairs
292 .iter()
293 .chain(leaf_node_keypairs.iter())
294 .collect();
295
296 let (new_keypairs, commit_secret) = diff.decrypt_path(
299 provider.crypto(),
300 &decryption_keypairs,
301 self.own_leaf_index(),
302 sender_index,
303 path.nodes(),
304 &apply_proposals_values.exclusion_list(),
305 )?;
306
307 let new_leaf_keypair_option = if let Some(leaf) = diff.leaf(self.own_leaf_index()) {
313 leaf_node_keypairs.into_iter().find_map(|keypair| {
314 if leaf.encryption_key() == keypair.public_key() {
315 Some(keypair)
316 } else {
317 None
318 }
319 })
320 } else {
321 debug_assert!(false);
323 None
324 };
325
326 let update_path_leaf_node = Some(path.leaf_node().clone());
330 debug_assert_eq!(diff.leaf(sender_index), path.leaf_node().into());
331
332 (
333 commit_secret,
334 new_keypairs,
335 new_leaf_keypair_option,
336 update_path_leaf_node,
337 )
338 } else {
339 if apply_proposals_values.path_required {
340 return Err(StageCommitError::RequiredPathNotFound);
342 }
343
344 diff.update_group_context(
346 provider.crypto(),
347 apply_proposals_values.extensions.clone(),
348 )?;
349
350 (CommitSecret::zero_secret(ciphersuite), vec![], None, None)
351 };
352
353 diff.update_confirmed_transcript_hash(provider.crypto(), mls_content)?;
355
356 let received_confirmation_tag = mls_content
357 .confirmation_tag()
358 .ok_or(StageCommitError::ConfirmationTagMissing)?;
359
360 let serialized_provisional_group_context = diff
361 .group_context()
362 .tls_serialize_detached()
363 .map_err(LibraryError::missing_bound_check)?;
364
365 let EpochSecretsResult {
366 epoch_secrets,
367 #[cfg(feature = "extensions-draft-08")]
368 application_exporter,
369 } = self.derive_epoch_secrets(
370 provider,
371 apply_proposals_values,
372 self.group_epoch_secrets(),
373 commit_secret,
374 &serialized_provisional_group_context,
375 )?;
376 let (provisional_group_secrets, provisional_message_secrets) = epoch_secrets.split_secrets(
377 serialized_provisional_group_context,
378 diff.tree_size(),
379 self.own_leaf_index(),
380 );
381
382 let own_confirmation_tag = provisional_message_secrets
385 .confirmation_key()
386 .tag(
387 provider.crypto(),
388 self.ciphersuite(),
389 diff.group_context().confirmed_transcript_hash(),
390 )
391 .map_err(LibraryError::unexpected_crypto_error)?;
392 if &own_confirmation_tag != received_confirmation_tag {
393 log::error!("Confirmation tag mismatch");
394 log_crypto!(trace, " Got: {:x?}", received_confirmation_tag);
395 log_crypto!(trace, " Expected: {:x?}", own_confirmation_tag);
396 if !crate::skip_validation::is_disabled::confirmation_tag() {
403 return Err(StageCommitError::ConfirmationTagMismatch);
404 }
405 }
406
407 diff.update_interim_transcript_hash(ciphersuite, provider.crypto(), own_confirmation_tag)?;
408
409 let staged_diff = diff.into_staged_diff(provider.crypto(), ciphersuite)?;
410 #[cfg(feature = "extensions-draft-08")]
411 let application_export_tree = ApplicationExportTree::new(application_exporter);
412 let staged_commit_state =
413 StagedCommitState::GroupMember(Box::new(MemberStagedCommitState::new(
414 provisional_group_secrets,
415 provisional_message_secrets,
416 staged_diff,
417 new_keypairs,
418 new_leaf_keypair_option,
419 update_path_leaf_node,
420 #[cfg(feature = "extensions-draft-08")]
421 application_export_tree,
422 )));
423 let staged_commit = StagedCommit::new(proposal_queue, staged_commit_state);
424
425 Ok(staged_commit)
426 }
427
428 pub(crate) fn merge_commit<Provider: OpenMlsProvider>(
434 &mut self,
435 provider: &Provider,
436 staged_commit: StagedCommit,
437 ) -> Result<(), MergeCommitError<Provider::StorageError>> {
438 let old_epoch_keypairs = self
441 .read_epoch_keypairs(provider.storage())
442 .map_err(MergeCommitError::StorageError)?;
443 match staged_commit.state {
444 StagedCommitState::PublicState(staged_state) => {
445 self.public_group
446 .merge_diff(staged_state.into_staged_diff());
447 self.store(provider.storage())
448 .map_err(MergeCommitError::StorageError)?;
449 Ok(())
450 }
451 StagedCommitState::GroupMember(state) => {
452 let past_epoch = self.context().epoch();
454 let leaves = self.public_group().members().collect();
456 self.group_epoch_secrets = state.group_epoch_secrets;
459
460 let old_message_secrets = self
462 .message_secrets_store
463 .replace_current_message_secrets(state.message_secrets);
464 self.message_secrets_store.add_past_epoch_tree(
465 past_epoch,
466 old_message_secrets,
467 leaves,
468 );
469
470 #[cfg(feature = "extensions-draft-08")]
472 {
473 if let Some(application_export_tree) = state.application_export_tree {
477 use openmls_traits::storage::StorageProvider as _;
480 provider
481 .storage()
482 .write_application_export_tree(
483 self.group_id(),
484 &application_export_tree,
485 )
486 .map_err(MergeCommitError::StorageError)?;
487
488 self.application_export_tree = Some(application_export_tree);
489 }
490 }
491
492 self.public_group.merge_diff(state.staged_diff);
493
494 let leaf_keypair = if let Some(keypair) = &state.new_leaf_keypair_option {
495 vec![keypair.clone()]
496 } else {
497 vec![]
498 };
499
500 let new_owned_encryption_keys = self
502 .public_group()
503 .owned_encryption_keys(self.own_leaf_index());
504 let epoch_keypairs: Vec<EncryptionKeyPair> = old_epoch_keypairs
506 .into_iter()
507 .chain(state.new_keypairs)
508 .chain(leaf_keypair)
509 .filter(|keypair| new_owned_encryption_keys.contains(keypair.public_key()))
510 .collect();
511
512 debug_assert_eq!(new_owned_encryption_keys.len(), epoch_keypairs.len());
514 if new_owned_encryption_keys.len() != epoch_keypairs.len() {
515 return Err(LibraryError::custom(
516 "We should have all the private key material we need.",
517 )
518 .into());
519 }
520
521 let storage = provider.storage();
523 let group_id = self.group_id();
524
525 self.public_group
526 .store(storage)
527 .map_err(MergeCommitError::StorageError)?;
528 storage
529 .write_group_epoch_secrets(group_id, &self.group_epoch_secrets)
530 .map_err(MergeCommitError::StorageError)?;
531 storage
532 .write_message_secrets(group_id, &self.message_secrets_store)
533 .map_err(MergeCommitError::StorageError)?;
534
535 self.store_epoch_keypairs(storage, epoch_keypairs.as_slice())
537 .map_err(MergeCommitError::StorageError)?;
538
539 self.delete_previous_epoch_keypairs(storage)
541 .map_err(MergeCommitError::StorageError)?;
542 if let Some(keypair) = state.new_leaf_keypair_option {
543 keypair
544 .delete(storage)
545 .map_err(MergeCommitError::StorageError)?;
546 }
547
548 storage
550 .clear_proposal_queue::<GroupId, ProposalRef>(group_id)
551 .map_err(MergeCommitError::StorageError)?;
552 self.proposal_store_mut().empty();
553
554 Ok(())
555 }
556 }
557 }
558}
559
560#[derive(Debug, Serialize, Deserialize)]
561#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
562pub(crate) enum StagedCommitState {
563 PublicState(Box<PublicStagedCommitState>),
564 GroupMember(Box<MemberStagedCommitState>),
566}
567
568#[derive(Debug, Serialize, Deserialize)]
570#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
571pub struct StagedCommit {
572 pub staged_proposal_queue: ProposalQueue,
574 pub(super) state: StagedCommitState,
576}
577
578impl StagedCommit {
579 pub(crate) fn new(staged_proposal_queue: ProposalQueue, state: StagedCommitState) -> Self {
582 StagedCommit {
583 staged_proposal_queue,
584 state,
585 }
586 }
587
588 pub fn epoch(&self) -> GroupEpoch {
590 self.group_context().epoch()
591 }
592
593 pub fn export_ratchet_tree(
595 &self,
596 crypto: &impl OpenMlsCrypto,
597 original_tree: RatchetTree,
598 ) -> Result<Option<RatchetTree>, TreeSyncFromNodesError> {
599 match &self.state {
600 StagedCommitState::PublicState(_public_staged_commit_state) => Ok(None),
601 StagedCommitState::GroupMember(member_staged_commit_state) => Ok(Some(
602 member_staged_commit_state.staged_diff.export_ratchet_tree(
603 crypto,
604 self.group_context().ciphersuite(),
605 original_tree,
606 )?,
607 )),
608 }
609 }
610
611 pub fn add_proposals(&self) -> impl Iterator<Item = QueuedAddProposal<'_>> {
613 self.staged_proposal_queue.add_proposals()
614 }
615
616 pub fn remove_proposals(&self) -> impl Iterator<Item = QueuedRemoveProposal<'_>> {
618 self.staged_proposal_queue.remove_proposals()
619 }
620
621 pub fn update_proposals(&self) -> impl Iterator<Item = QueuedUpdateProposal<'_>> {
623 self.staged_proposal_queue.update_proposals()
624 }
625
626 pub fn psk_proposals(&self) -> impl Iterator<Item = QueuedPskProposal<'_>> {
628 self.staged_proposal_queue.psk_proposals()
629 }
630
631 #[cfg(feature = "extensions-draft-08")]
632 pub fn queued_app_ephemeral_proposals(
635 &self,
636 ) -> impl Iterator<Item = QueuedAppEphemeralProposal<'_>> {
637 self.staged_proposal_queue.app_ephemeral_proposals()
638 }
639 #[cfg(feature = "extensions-draft-08")]
641 pub fn app_data_update_proposals(
644 &self,
645 ) -> impl Iterator<Item = QueuedAppDataUpdateProposal<'_>> {
646 self.staged_proposal_queue.app_data_update_proposals()
647 }
648
649 pub fn queued_proposals(&self) -> impl Iterator<Item = &QueuedProposal> {
651 self.staged_proposal_queue.queued_proposals()
652 }
653
654 pub fn update_path_leaf_node(&self) -> Option<&LeafNode> {
656 match self.state {
657 StagedCommitState::PublicState(ref public_state) => {
658 public_state.update_path_leaf_node()
659 }
660 StagedCommitState::GroupMember(ref group_member_state) => {
661 group_member_state.update_path_leaf_node.as_ref()
662 }
663 }
664 }
665
666 pub fn credentials_to_verify(&self) -> impl Iterator<Item = &Credential> {
668 let update_path_leaf_node_cred = if let Some(node) = self.update_path_leaf_node() {
669 vec![node.credential()]
670 } else {
671 vec![]
672 };
673
674 update_path_leaf_node_cred
675 .into_iter()
676 .chain(
677 self.queued_proposals()
678 .flat_map(|proposal: &QueuedProposal| match proposal.proposal() {
679 Proposal::Update(update_proposal) => {
680 vec![update_proposal.leaf_node().credential()].into_iter()
681 }
682 Proposal::Add(add_proposal) => {
683 vec![add_proposal.key_package().leaf_node().credential()].into_iter()
684 }
685 Proposal::GroupContextExtensions(gce_proposal) => gce_proposal
686 .extensions()
687 .iter()
688 .flat_map(|extension| {
689 match extension {
690 Extension::ExternalSenders(external_senders) => {
691 external_senders
692 .iter()
693 .map(|external_sender| external_sender.credential())
694 .collect()
695 }
696 _ => vec![],
697 }
698 .into_iter()
699 })
700 .collect::<Vec<_>>()
707 .into_iter(),
708 _ => vec![].into_iter(),
709 }),
710 )
711 }
712
713 pub fn self_removed(&self) -> bool {
716 matches!(self.state, StagedCommitState::PublicState(_))
717 }
718
719 pub fn group_context(&self) -> &GroupContext {
721 match self.state {
722 StagedCommitState::PublicState(ref ps) => ps.staged_diff().group_context(),
723 StagedCommitState::GroupMember(ref gm) => gm.group_context(),
724 }
725 }
726 pub(crate) fn into_state(self) -> StagedCommitState {
728 self.state
729 }
730
731 pub fn epoch_authenticator(&self) -> Option<&EpochAuthenticator> {
735 if let StagedCommitState::GroupMember(ref gm) = self.state {
736 Some(gm.group_epoch_secrets.epoch_authenticator())
737 } else {
738 None
739 }
740 }
741
742 pub fn resumption_psk_secret(&self) -> Option<&ResumptionPskSecret> {
746 if let StagedCommitState::GroupMember(ref gm) = self.state {
747 Some(gm.group_epoch_secrets.resumption_psk())
748 } else {
749 None
750 }
751 }
752
753 #[cfg(feature = "extensions-draft-08")]
754 pub(crate) fn safe_export_secret(
755 &mut self,
756 crypto: &impl OpenMlsCrypto,
757 component_id: u16,
758 ) -> Result<Vec<u8>, StagedSafeExportSecretError> {
759 let ciphersuite = self.group_context().ciphersuite();
760 let StagedCommitState::GroupMember(ref mut staged_commit) = self.state else {
761 return Err(StagedSafeExportSecretError::NotGroupMember);
762 };
763 let Some(application_export_tree) = staged_commit.application_export_tree.as_mut() else {
764 return Err(StagedSafeExportSecretError::Unsupported);
765 };
766 let secret =
767 application_export_tree.safe_export_secret(crypto, ciphersuite, component_id)?;
768 Ok(secret.as_slice().to_vec())
769 }
770
771 pub fn export_secret<CryptoProvider: OpenMlsCrypto>(
779 &self,
780 crypto: &CryptoProvider,
781 label: &str,
782 context: &[u8],
783 key_length: usize,
784 ) -> Result<Vec<u8>, ExportSecretError> {
785 if key_length > u16::MAX as usize {
786 log::error!("Got a key that is larger than u16::MAX");
787 return Err(ExportSecretError::KeyLengthTooLong);
788 }
789
790 match &self.state {
791 StagedCommitState::PublicState(_public_staged_commit_state) => Err(
792 ExportSecretError::GroupStateError(MlsGroupStateError::UseAfterEviction),
793 ),
794 StagedCommitState::GroupMember(member_staged_commit_state) => {
795 Ok(member_staged_commit_state
796 .group_epoch_secrets
797 .exporter_secret()
798 .derive_exported_secret(
799 self.group_context().ciphersuite(),
800 crypto,
801 label,
802 context,
803 key_length,
804 )
805 .map_err(LibraryError::unexpected_crypto_error)?)
806 }
807 }
808 }
809}
810
811#[derive(Debug, Serialize, Deserialize)]
813#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
814pub(crate) struct MemberStagedCommitState {
815 group_epoch_secrets: GroupEpochSecrets,
816 message_secrets: MessageSecrets,
817 staged_diff: StagedPublicGroupDiff,
818 new_keypairs: Vec<EncryptionKeyPair>,
819 new_leaf_keypair_option: Option<EncryptionKeyPair>,
820 update_path_leaf_node: Option<LeafNode>,
821 #[cfg(feature = "extensions-draft-08")]
822 #[serde(default)]
823 application_export_tree: Option<ApplicationExportTree>,
826}
827
828impl MemberStagedCommitState {
829 pub(crate) fn new(
830 group_epoch_secrets: GroupEpochSecrets,
831 message_secrets: MessageSecrets,
832 staged_diff: StagedPublicGroupDiff,
833 new_keypairs: Vec<EncryptionKeyPair>,
834 new_leaf_keypair_option: Option<EncryptionKeyPair>,
835 update_path_leaf_node: Option<LeafNode>,
836 #[cfg(feature = "extensions-draft-08")] application_export_tree: ApplicationExportTree,
837 ) -> Self {
838 Self {
839 group_epoch_secrets,
840 message_secrets,
841 staged_diff,
842 new_keypairs,
843 new_leaf_keypair_option,
844 update_path_leaf_node,
845 #[cfg(feature = "extensions-draft-08")]
846 application_export_tree: Some(application_export_tree),
847 }
848 }
849
850 pub(crate) fn group_context(&self) -> &GroupContext {
852 self.staged_diff.group_context()
853 }
854}