Skip to main content

openmls/group/mls_group/
staged_commit.rs

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        // Check if we need to include the init secret from an external commit
54        // we applied earlier or if we use the one from the previous epoch.
55        let joiner_secret = if let Some(ref external_init_proposal) =
56            apply_proposals_values.external_init_proposal_option
57        {
58            // Decrypt the content and derive the external init secret.
59            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        // Prepare the PskSecret
91        // Fails if PSKs are missing ([valn1205](https://validation.openmls.tech/#valn1205))
92        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        // Create key schedule
103        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    /// Stages a commit message that was sent by another group member. This
119    /// function does the following:
120    ///  - Applies the proposals covered by the commit to the tree
121    ///  - Applies the (optional) update path to the tree
122    ///  - Decrypts and calculates the path secrets
123    ///  - Initializes the key schedule for epoch rollover
124    ///  - Verifies the confirmation tag
125    ///
126    /// Returns a [StagedCommit] that can be inspected and later merged into the
127    /// group state with [MlsGroup::merge_commit()]. If the member was not
128    /// removed from the group, the function also returns an
129    /// [ApplicationExportSecret].
130    ///
131    /// This function does the following checks:
132    ///  - ValSem101
133    ///  - ValSem102
134    ///  - ValSem104
135    ///  - ValSem105
136    ///  - ValSem106
137    ///  - ValSem107
138    ///  - ValSem108
139    ///  - ValSem110
140    ///  - ValSem111
141    ///  - ValSem112
142    ///  - ValSem113: All Proposals: The proposal type must be supported by all
143    ///    members of the group
144    ///  - ValSem200
145    ///  - ValSem201
146    ///  - ValSem202: Path must be the right length
147    ///  - ValSem203: Path secrets must decrypt correctly
148    ///  - ValSem204: Public keys from Path must be verified and match the
149    ///    private keys from the direct path
150    ///  - ValSem205
151    ///  - ValSem240
152    ///  - ValSem241
153    ///  - ValSem242
154    ///  - ValSem244 Returns an error if the given commit was sent by the owner
155    ///    of this group.
156    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        // Check that the sender is another member of the group
164        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        // Create the provisional public group state (including the tree and
175        // group context) and apply proposals.
176        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        // Check that the sender is another member of the group
211        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        // Create the provisional public group state (including the tree and
222        // group context) and apply proposals.
223        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        // Determine if Commit has a path
259        let (commit_secret, new_keypairs, new_leaf_keypair_option, update_path_leaf_node) =
260            if let Some(path) = commit.path.clone() {
261                // Update the public group
262                // ValSem202: Path must be the right length
263                diff.apply_received_update_path(
264                    provider.crypto(),
265                    ciphersuite,
266                    sender_index,
267                    &path,
268                )?;
269
270                // Update group context
271                diff.update_group_context(
272                    provider.crypto(),
273                    apply_proposals_values.extensions.clone(),
274                )?;
275
276                // Check if we were removed from the group
277                if apply_proposals_values.self_removed {
278                    // If so, we return here, because we can't decrypt the path
279                    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                // ValSem203: Path secrets must decrypt correctly
297                // ValSem204: Public keys from Path must be verified and match the private keys from the direct path
298                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                // Check if one of our update proposals was applied. If so, we
308                // need to store that keypair separately, because after merging
309                // it needs to be removed from the key store separately and in
310                // addition to the removal of the keypairs of the previous
311                // epoch.
312                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                    // We should have an own leaf at this point.
322                    debug_assert!(false);
323                    None
324                };
325
326                // Return the leaf node in the update path so the credential can be validated.
327                // Since the diff has already been updated, this should be the same as the leaf
328                // at the sender index.
329                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                    // ValSem201
341                    return Err(StageCommitError::RequiredPathNotFound);
342                }
343
344                // Even if there is no path, we have to update the group context.
345                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        // Update the confirmed transcript hash before we compute the confirmation tag.
354        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        // Verify confirmation tag
383        // ValSem205
384        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            // TODO: We have tests expecting this error.
397            //       They need to be rewritten.
398            // debug_assert!(false, "Confirmation tag mismatch");
399
400            // in some tests we need to be able to proceed despite the tag being wrong,
401            // e.g. to test whether a later validation check is performed correctly.
402            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    /// Merges a [StagedCommit] into the group state and optionally return a [`SecretTree`]
429    /// from the previous epoch. The secret tree is returned if the Commit does not contain a self removal.
430    ///
431    /// This function should not fail and only returns a [`Result`], because it
432    /// might throw a `LibraryError`.
433    pub(crate) fn merge_commit<Provider: OpenMlsProvider>(
434        &mut self,
435        provider: &Provider,
436        staged_commit: StagedCommit,
437    ) -> Result<(), MergeCommitError<Provider::StorageError>> {
438        // Get all keypairs from the old epoch, so we can later store the ones
439        // that are still relevant in the new epoch.
440        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                // Save the past epoch
453                let past_epoch = self.context().epoch();
454                // Get all the full leaves
455                let leaves = self.public_group().members().collect();
456                // Merge the staged commit into the group state and store the secret tree from the
457                // previous epoch in the message secrets store.
458                self.group_epoch_secrets = state.group_epoch_secrets;
459
460                // Replace the previous message secrets with the new ones and return the previous message secrets
461                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                // Replace the previous exporter tree with the new one.
471                #[cfg(feature = "extensions-draft-08")]
472                {
473                    // The application exporter is only None if the group was
474                    // stored using an older version of OpenMLS that did not
475                    // support the application exporter.
476                    if let Some(application_export_tree) = state.application_export_tree {
477                        // Overwrite the existing exporter tree in the storage.
478
479                        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                // Figure out which keys we need in the new epoch.
501                let new_owned_encryption_keys = self
502                    .public_group()
503                    .owned_encryption_keys(self.own_leaf_index());
504                // From the old and new keys, keep the ones that are still relevant in the new epoch.
505                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                // We should have private keys for all owned encryption keys.
513                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                // Store the updated group state
522                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                // Store the relevant keys under the new epoch
536                self.store_epoch_keypairs(storage, epoch_keypairs.as_slice())
537                    .map_err(MergeCommitError::StorageError)?;
538
539                // Delete the old keys.
540                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                // Empty the proposal store
549                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    /// The group member variant of the staged commit state.
565    GroupMember(Box<MemberStagedCommitState>),
566}
567
568/// Contains the changes from a commit to the group state.
569#[derive(Debug, Serialize, Deserialize)]
570#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
571pub struct StagedCommit {
572    /// A queue containing the proposals associated with the commit.
573    pub staged_proposal_queue: ProposalQueue,
574    /// The staged commit state.
575    pub(super) state: StagedCommitState,
576}
577
578impl StagedCommit {
579    /// Create a new [`StagedCommit`] from the provisional group state created
580    /// during the commit process.
581    pub(crate) fn new(staged_proposal_queue: ProposalQueue, state: StagedCommitState) -> Self {
582        StagedCommit {
583            staged_proposal_queue,
584            state,
585        }
586    }
587
588    /// Returns the epoch that this commit moves the group into
589    pub fn epoch(&self) -> GroupEpoch {
590        self.group_context().epoch()
591    }
592
593    /// Returns the ratchet tree of the staged commit state.
594    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    /// Returns the Add proposals that are covered by the Commit message as in iterator over [QueuedAddProposal].
612    pub fn add_proposals(&self) -> impl Iterator<Item = QueuedAddProposal<'_>> {
613        self.staged_proposal_queue.add_proposals()
614    }
615
616    /// Returns the Remove proposals that are covered by the Commit message as in iterator over [QueuedRemoveProposal].
617    pub fn remove_proposals(&self) -> impl Iterator<Item = QueuedRemoveProposal<'_>> {
618        self.staged_proposal_queue.remove_proposals()
619    }
620
621    /// Returns the Update proposals that are covered by the Commit message as in iterator over [QueuedUpdateProposal].
622    pub fn update_proposals(&self) -> impl Iterator<Item = QueuedUpdateProposal<'_>> {
623        self.staged_proposal_queue.update_proposals()
624    }
625
626    /// Returns the PresharedKey proposals that are covered by the Commit message as in iterator over [QueuedPskProposal].
627    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    /// Returns the AppEphemeral proposals that are covered by the Commit message as an iterator
633    /// over [`QueuedAppEphemeralProposal`].
634    pub fn queued_app_ephemeral_proposals(
635        &self,
636    ) -> impl Iterator<Item = QueuedAppEphemeralProposal<'_>> {
637        self.staged_proposal_queue.app_ephemeral_proposals()
638    }
639    // NOTE: this is not a default proposal type
640    #[cfg(feature = "extensions-draft-08")]
641    /// Returns the AppDataUpdate proposals that are covered by the Commit message as an iterator
642    /// over [`QueuedAppDataUpdateProposal`].
643    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    /// Returns an iterator over all [`QueuedProposal`]s.
650    pub fn queued_proposals(&self) -> impl Iterator<Item = &QueuedProposal> {
651        self.staged_proposal_queue.queued_proposals()
652    }
653
654    /// Returns the leaf node of the (optional) update path.
655    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    /// Returns the credentials that the caller needs to verify are valid.
667    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                            // TODO: ideally we wouldn't collect in between here, but the match arms
701                            //       have to all return the same type. We solve this by having them all
702                            //       be vec::IntoIter, but it would be nice if we just didn't have to
703                            //       do this.
704                            //       It might be possible to solve this by letting all match arms
705                            //       evaluate to a dyn Iterator.
706                            .collect::<Vec<_>>()
707                            .into_iter(),
708                        _ => vec![].into_iter(),
709                    }),
710            )
711    }
712
713    /// Returns `true` if the member was removed through a proposal covered by this Commit message
714    /// and `false` otherwise.
715    pub fn self_removed(&self) -> bool {
716        matches!(self.state, StagedCommitState::PublicState(_))
717    }
718
719    /// Returns the [`GroupContext`] of the staged commit state.
720    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    /// Consume this [`StagedCommit`] and return the internal [`StagedCommitState`].
727    pub(crate) fn into_state(self) -> StagedCommitState {
728        self.state
729    }
730
731    /// Returns the [`EpochAuthenticator`] of the staged commit state if the
732    /// owner of the originating group state is a member of the group. Returns
733    /// `None` otherwise.
734    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    /// Returns the [`ResumptionPskSecret`] of the staged commit state if the
743    /// owner of the originating group state is a member of the group. Returns
744    /// `None` otherwise.
745    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    /// Exports a secret from the epoch that the staged commit moves to.
772    /// Returns [`ExportSecretError::KeyLengthTooLong`] if the requested
773    /// key length is too long.
774    /// Returns [`ExportSecretError::GroupStateError(MlsGroupStateError::UseAfterEviction)`]
775    /// if the commit removed us from the group.
776    ///
777    /// [`ExportSecretError::GroupStateError(MlsGroupStateError::UseAfterEviction)`]: MlsGroupStateError::UseAfterEviction
778    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/// This struct is used internally by [`StagedCommit`] to encapsulate all the modified group state.
812#[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    // This is `None` only if the group was stored using an older version of
824    // OpenMLS that did not support the application exporter.
825    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    /// Get the staged [`GroupContext`].
851    pub(crate) fn group_context(&self) -> &GroupContext {
852        self.staged_diff.group_context()
853    }
854}