1use core::fmt::Debug;
2use std::mem;
3
4use openmls_traits::crypto::OpenMlsCrypto;
5use openmls_traits::storage::StorageProvider as _;
6use serde::{Deserialize, Serialize};
7use tls_codec::Serialize as _;
8
9use super::proposal_store::{
10 QueuedAddProposal, QueuedPskProposal, QueuedRemoveProposal, QueuedUpdateProposal,
11};
12
13use super::{
14 super::errors::*, load_psks, Credential, Extension, GroupContext, GroupEpochSecrets, GroupId,
15 JoinerSecret, KeySchedule, LeafNode, LibraryError, MessageSecrets, MlsGroup, OpenMlsProvider,
16 Proposal, ProposalQueue, PskSecret, QueuedProposal, Sender,
17};
18use crate::group::diff::PublicGroupDiff;
19use crate::group::GroupEpoch;
20use crate::prelude::{Commit, LeafNodeIndex};
21#[cfg(feature = "extensions-draft-08")]
22use crate::schedule::application_export_tree::ApplicationExportTree;
23
24use crate::treesync::errors::TreeSyncFromNodesError;
25use crate::treesync::RatchetTree;
26use crate::{
27 ciphersuite::{hash_ref::ProposalRef, Secret},
28 framing::mls_auth_content::AuthenticatedContent,
29 group::public_group::{
30 diff::{apply_proposals::ApplyProposalsValues, StagedPublicGroupDiff},
31 staged_commit::PublicStagedCommitState,
32 },
33 schedule::{
34 CommitSecret, EpochAuthenticator, EpochSecretsResult, InitSecret, PreSharedKeyId,
35 ResumptionPskSecret,
36 },
37 treesync::node::encryption_keys::EncryptionKeyPair,
38};
39
40#[cfg(feature = "extensions-draft-08")]
41use super::proposal_store::{QueuedAppDataUpdateProposal, QueuedAppEphemeralProposal};
42#[cfg(feature = "extensions-draft-08")]
43use crate::prelude::processing::AppDataUpdates;
44
45impl MlsGroup {
46 fn derive_epoch_secrets(
47 &self,
48 provider: &impl OpenMlsProvider,
49 apply_proposals_values: ApplyProposalsValues,
50 epoch_secrets: &GroupEpochSecrets,
51 commit_secret: CommitSecret,
52 serialized_provisional_group_context: &[u8],
53 ) -> Result<EpochSecretsResult, StageCommitError> {
54 let joiner_secret = if let Some(ref external_init_proposal) =
57 apply_proposals_values.external_init_proposal_option
58 {
59 let external_priv = epoch_secrets
61 .external_secret()
62 .derive_external_keypair(provider.crypto(), self.ciphersuite())
63 .map_err(LibraryError::unexpected_crypto_error)?
64 .private;
65 let init_secret = InitSecret::from_kem_output(
66 provider.crypto(),
67 self.ciphersuite(),
68 self.version(),
69 &external_priv,
70 external_init_proposal.kem_output(),
71 )?;
72 JoinerSecret::new(
73 provider.crypto(),
74 self.ciphersuite(),
75 commit_secret,
76 &init_secret,
77 serialized_provisional_group_context,
78 )
79 .map_err(LibraryError::unexpected_crypto_error)?
80 } else {
81 JoinerSecret::new(
82 provider.crypto(),
83 self.ciphersuite(),
84 commit_secret,
85 epoch_secrets.init_secret(),
86 serialized_provisional_group_context,
87 )
88 .map_err(LibraryError::unexpected_crypto_error)?
89 };
90
91 let psk_secret = {
94 let psks: Vec<(&PreSharedKeyId, Secret)> = load_psks(
95 provider.storage(),
96 &self.resumption_psk_store,
97 &apply_proposals_values.presharedkeys,
98 )?;
99
100 PskSecret::new(provider.crypto(), self.ciphersuite(), psks)?
101 };
102
103 let mut key_schedule = KeySchedule::init(
105 self.ciphersuite(),
106 provider.crypto(),
107 &joiner_secret,
108 psk_secret,
109 )?;
110
111 key_schedule
112 .add_context(provider.crypto(), serialized_provisional_group_context)
113 .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
114 Ok(key_schedule
115 .epoch_secrets(provider.crypto(), self.ciphersuite())
116 .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?)
117 }
118
119 pub(crate) fn stage_commit(
158 &self,
159 mls_content: &AuthenticatedContent,
160 old_epoch_keypairs: Vec<EncryptionKeyPair>,
161 leaf_node_keypairs: Vec<EncryptionKeyPair>,
162 provider: &impl OpenMlsProvider,
163 ) -> Result<StagedCommit, StageCommitError> {
164 if let Sender::Member(member) = mls_content.sender() {
166 if member == &self.own_leaf_index() {
167 return Err(StageCommitError::OwnCommit);
168 }
169 }
170
171 let (commit, proposal_queue, sender_index) = self
172 .public_group
173 .validate_commit(mls_content, provider.crypto())?;
174
175 let mut diff = self.public_group.empty_diff();
178
179 #[cfg(not(feature = "extensions-draft-08"))]
180 let apply_proposals_values =
181 diff.apply_proposals(&proposal_queue, self.own_leaf_index())?;
182
183 #[cfg(feature = "extensions-draft-08")]
184 let apply_proposals_values = diff.apply_proposals_with_app_data_updates(
185 &proposal_queue,
186 self.own_leaf_index(),
187 None,
188 )?;
189 self.stage_applied_proposal_values(
190 apply_proposals_values,
191 diff,
192 commit,
193 proposal_queue,
194 sender_index,
195 mls_content,
196 old_epoch_keypairs,
197 leaf_node_keypairs,
198 provider,
199 )
200 }
201
202 #[cfg(feature = "extensions-draft-08")]
203 pub(crate) fn stage_commit_with_app_data_updates(
204 &self,
205 mls_content: &AuthenticatedContent,
206 old_epoch_keypairs: Vec<EncryptionKeyPair>,
207 leaf_node_keypairs: Vec<EncryptionKeyPair>,
208 app_data_dict_updates: Option<AppDataUpdates>,
209 provider: &impl OpenMlsProvider,
210 ) -> Result<StagedCommit, StageCommitError> {
211 if let Sender::Member(member) = mls_content.sender() {
213 if member == &self.own_leaf_index() {
214 return Err(StageCommitError::OwnCommit);
215 }
216 }
217
218 let (commit, proposal_queue, sender_index) = self
219 .public_group
220 .validate_commit(mls_content, provider.crypto())?;
221
222 let mut diff = self.public_group.empty_diff();
225
226 let apply_proposals_values = diff.apply_proposals_with_app_data_updates(
227 &proposal_queue,
228 self.own_leaf_index(),
229 app_data_dict_updates,
230 )?;
231
232 self.stage_applied_proposal_values(
233 apply_proposals_values,
234 diff,
235 commit,
236 proposal_queue,
237 sender_index,
238 mls_content,
239 old_epoch_keypairs,
240 leaf_node_keypairs,
241 provider,
242 )
243 }
244
245 #[allow(clippy::too_many_arguments)]
246 fn stage_applied_proposal_values(
247 &self,
248 apply_proposals_values: ApplyProposalsValues,
249 mut diff: PublicGroupDiff,
250 commit: &Commit,
251 proposal_queue: ProposalQueue,
252 sender_index: LeafNodeIndex,
253 mls_content: &AuthenticatedContent,
254 old_epoch_keypairs: Vec<EncryptionKeyPair>,
255 leaf_node_keypairs: Vec<EncryptionKeyPair>,
256 provider: &impl OpenMlsProvider,
257 ) -> Result<StagedCommit, StageCommitError> {
258 let ciphersuite = self.ciphersuite();
259 let (commit_secret, new_keypairs, new_leaf_keypair_option, update_path_leaf_node) =
261 if let Some(path) = commit.path.clone() {
262 diff.apply_received_update_path(
265 provider.crypto(),
266 ciphersuite,
267 sender_index,
268 &path,
269 )?;
270
271 diff.update_group_context(
273 provider.crypto(),
274 apply_proposals_values.extensions.clone(),
275 )?;
276
277 if apply_proposals_values.self_removed {
279 let staged_diff = diff.into_staged_diff(provider.crypto(), ciphersuite)?;
281 let staged_state = PublicStagedCommitState::new(
282 staged_diff,
283 commit.path.as_ref().map(|path| path.leaf_node().clone()),
284 );
285 let staged_commit = StagedCommit::new(
286 proposal_queue,
287 StagedCommitState::PublicState(Box::new(staged_state)),
288 );
289 return Ok(staged_commit);
290 }
291
292 let decryption_keypairs: Vec<&EncryptionKeyPair> = old_epoch_keypairs
293 .iter()
294 .chain(leaf_node_keypairs.iter())
295 .collect();
296
297 let (new_keypairs, commit_secret) = diff.decrypt_path(
300 provider.crypto(),
301 &decryption_keypairs,
302 self.own_leaf_index(),
303 sender_index,
304 path.nodes(),
305 &apply_proposals_values.exclusion_list(),
306 )?;
307
308 let new_leaf_keypair_option = if let Some(leaf) = diff.leaf(self.own_leaf_index()) {
314 leaf_node_keypairs.into_iter().find_map(|keypair| {
315 if leaf.encryption_key() == keypair.public_key() {
316 Some(keypair)
317 } else {
318 None
319 }
320 })
321 } else {
322 debug_assert!(false);
324 None
325 };
326
327 let update_path_leaf_node = Some(path.leaf_node().clone());
331 debug_assert_eq!(diff.leaf(sender_index), path.leaf_node().into());
332
333 (
334 commit_secret,
335 new_keypairs,
336 new_leaf_keypair_option,
337 update_path_leaf_node,
338 )
339 } else {
340 if apply_proposals_values.path_required {
341 return Err(StageCommitError::RequiredPathNotFound);
343 }
344
345 diff.update_group_context(
347 provider.crypto(),
348 apply_proposals_values.extensions.clone(),
349 )?;
350
351 (CommitSecret::zero_secret(ciphersuite), vec![], None, None)
352 };
353
354 diff.update_confirmed_transcript_hash(provider.crypto(), mls_content)?;
356
357 let received_confirmation_tag = mls_content
358 .confirmation_tag()
359 .ok_or(StageCommitError::ConfirmationTagMissing)?;
360
361 let serialized_provisional_group_context = diff
362 .group_context()
363 .tls_serialize_detached()
364 .map_err(LibraryError::missing_bound_check)?;
365
366 let EpochSecretsResult {
367 epoch_secrets,
368 #[cfg(feature = "extensions-draft-08")]
369 application_exporter,
370 } = self.derive_epoch_secrets(
371 provider,
372 apply_proposals_values,
373 self.group_epoch_secrets(),
374 commit_secret,
375 &serialized_provisional_group_context,
376 )?;
377 let (provisional_group_secrets, provisional_message_secrets) = epoch_secrets.split_secrets(
378 serialized_provisional_group_context,
379 diff.tree_size(),
380 self.own_leaf_index(),
381 );
382
383 let own_confirmation_tag = provisional_message_secrets
386 .confirmation_key()
387 .tag(
388 provider.crypto(),
389 self.ciphersuite(),
390 diff.group_context().confirmed_transcript_hash(),
391 )
392 .map_err(LibraryError::unexpected_crypto_error)?;
393 if &own_confirmation_tag != received_confirmation_tag {
394 log::error!("Confirmation tag mismatch");
395 log_crypto!(trace, " Got: {:x?}", received_confirmation_tag);
396 log_crypto!(trace, " Expected: {:x?}", own_confirmation_tag);
397 if !crate::skip_validation::is_disabled::confirmation_tag() {
404 return Err(StageCommitError::ConfirmationTagMismatch);
405 }
406 }
407
408 diff.update_interim_transcript_hash(ciphersuite, provider.crypto(), own_confirmation_tag)?;
409
410 let staged_diff = diff.into_staged_diff(provider.crypto(), ciphersuite)?;
411 #[cfg(feature = "extensions-draft-08")]
412 let application_export_tree = ApplicationExportTree::new(application_exporter);
413 let staged_commit_state =
414 StagedCommitState::GroupMember(Box::new(MemberStagedCommitState::new(
415 provisional_group_secrets,
416 provisional_message_secrets,
417 staged_diff,
418 new_keypairs,
419 new_leaf_keypair_option,
420 update_path_leaf_node,
421 #[cfg(feature = "extensions-draft-08")]
422 application_export_tree,
423 )));
424 let staged_commit = StagedCommit::new(proposal_queue, staged_commit_state);
425
426 Ok(staged_commit)
427 }
428
429 pub(crate) fn merge_commit<Provider: OpenMlsProvider>(
435 &mut self,
436 provider: &Provider,
437 staged_commit: StagedCommit,
438 ) -> Result<(), MergeCommitError<Provider::StorageError>> {
439 let old_epoch_keypairs = self
442 .read_epoch_keypairs(provider.storage())
443 .map_err(MergeCommitError::StorageError)?;
444 match staged_commit.state {
445 StagedCommitState::PublicState(staged_state) => {
446 self.public_group
447 .merge_diff(staged_state.into_staged_diff());
448 self.store(provider.storage())
449 .map_err(MergeCommitError::StorageError)?;
450 Ok(())
451 }
452 StagedCommitState::GroupMember(state) => {
453 let past_epoch = self.context().epoch();
455 let leaves = self.public_group().members().collect();
457 self.group_epoch_secrets = state.group_epoch_secrets;
460
461 let mut message_secrets = state.message_secrets;
463 mem::swap(
464 &mut message_secrets,
465 self.message_secrets_store.message_secrets_mut(),
466 );
467 self.message_secrets_store
468 .add(past_epoch, message_secrets, leaves);
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}