Skip to main content

openmls/group/mls_group/
staged_commit.rs

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        // Check if we need to include the init secret from an external commit
55        // we applied earlier or if we use the one from the previous epoch.
56        let joiner_secret = if let Some(ref external_init_proposal) =
57            apply_proposals_values.external_init_proposal_option
58        {
59            // Decrypt the content and derive the external init secret.
60            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        // Prepare the PskSecret
92        // Fails if PSKs are missing ([valn1205](https://validation.openmls.tech/#valn1205))
93        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        // Create key schedule
104        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    /// Stages a commit message that was sent by another group member. This
120    /// function does the following:
121    ///  - Applies the proposals covered by the commit to the tree
122    ///  - Applies the (optional) update path to the tree
123    ///  - Decrypts and calculates the path secrets
124    ///  - Initializes the key schedule for epoch rollover
125    ///  - Verifies the confirmation tag
126    ///
127    /// Returns a [StagedCommit] that can be inspected and later merged into the
128    /// group state with [MlsGroup::merge_commit()]. If the member was not
129    /// removed from the group, the function also returns an
130    /// [ApplicationExportSecret].
131    ///
132    /// This function does the following checks:
133    ///  - ValSem101
134    ///  - ValSem102
135    ///  - ValSem104
136    ///  - ValSem105
137    ///  - ValSem106
138    ///  - ValSem107
139    ///  - ValSem108
140    ///  - ValSem110
141    ///  - ValSem111
142    ///  - ValSem112
143    ///  - ValSem113: All Proposals: The proposal type must be supported by all
144    ///    members of the group
145    ///  - ValSem200
146    ///  - ValSem201
147    ///  - ValSem202: Path must be the right length
148    ///  - ValSem203: Path secrets must decrypt correctly
149    ///  - ValSem204: Public keys from Path must be verified and match the
150    ///    private keys from the direct path
151    ///  - ValSem205
152    ///  - ValSem240
153    ///  - ValSem241
154    ///  - ValSem242
155    ///  - ValSem244 Returns an error if the given commit was sent by the owner
156    ///    of this group.
157    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        // Check that the sender is another member of the group
165        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        // Create the provisional public group state (including the tree and
176        // group context) and apply proposals.
177        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        // Check that the sender is another member of the group
212        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        // Create the provisional public group state (including the tree and
223        // group context) and apply proposals.
224        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        // Determine if Commit has a path
260        let (commit_secret, new_keypairs, new_leaf_keypair_option, update_path_leaf_node) =
261            if let Some(path) = commit.path.clone() {
262                // Update the public group
263                // ValSem202: Path must be the right length
264                diff.apply_received_update_path(
265                    provider.crypto(),
266                    ciphersuite,
267                    sender_index,
268                    &path,
269                )?;
270
271                // Update group context
272                diff.update_group_context(
273                    provider.crypto(),
274                    apply_proposals_values.extensions.clone(),
275                )?;
276
277                // Check if we were removed from the group
278                if apply_proposals_values.self_removed {
279                    // If so, we return here, because we can't decrypt the path
280                    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                // ValSem203: Path secrets must decrypt correctly
298                // ValSem204: Public keys from Path must be verified and match the private keys from the direct path
299                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                // Check if one of our update proposals was applied. If so, we
309                // need to store that keypair separately, because after merging
310                // it needs to be removed from the key store separately and in
311                // addition to the removal of the keypairs of the previous
312                // epoch.
313                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                    // We should have an own leaf at this point.
323                    debug_assert!(false);
324                    None
325                };
326
327                // Return the leaf node in the update path so the credential can be validated.
328                // Since the diff has already been updated, this should be the same as the leaf
329                // at the sender index.
330                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                    // ValSem201
342                    return Err(StageCommitError::RequiredPathNotFound);
343                }
344
345                // Even if there is no path, we have to update the group context.
346                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        // Update the confirmed transcript hash before we compute the confirmation tag.
355        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        // Verify confirmation tag
384        // ValSem205
385        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            // TODO: We have tests expecting this error.
398            //       They need to be rewritten.
399            // debug_assert!(false, "Confirmation tag mismatch");
400
401            // in some tests we need to be able to proceed despite the tag being wrong,
402            // e.g. to test whether a later validation check is performed correctly.
403            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    /// Merges a [StagedCommit] into the group state and optionally return a [`SecretTree`]
430    /// from the previous epoch. The secret tree is returned if the Commit does not contain a self removal.
431    ///
432    /// This function should not fail and only returns a [`Result`], because it
433    /// might throw a `LibraryError`.
434    pub(crate) fn merge_commit<Provider: OpenMlsProvider>(
435        &mut self,
436        provider: &Provider,
437        staged_commit: StagedCommit,
438    ) -> Result<(), MergeCommitError<Provider::StorageError>> {
439        // Get all keypairs from the old epoch, so we can later store the ones
440        // that are still relevant in the new epoch.
441        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                // Save the past epoch
454                let past_epoch = self.context().epoch();
455                // Get all the full leaves
456                let leaves = self.public_group().members().collect();
457                // Merge the staged commit into the group state and store the secret tree from the
458                // previous epoch in the message secrets store.
459                self.group_epoch_secrets = state.group_epoch_secrets;
460
461                // Replace the previous message secrets with the new ones and return the previous message secrets
462                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                // 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}