Skip to main content

openmls/group/mls_group/
processing.rs

1//! Processing functions of an [`MlsGroup`] for incoming messages.
2
3use std::mem;
4
5use errors::{CommitToPendingProposalsError, MergePendingCommitError};
6use openmls_traits::{crypto::OpenMlsCrypto, signatures::Signer, storage::StorageProvider as _};
7
8use crate::{
9    framing::mls_content::FramedContentBody,
10    group::{errors::MergeCommitError, StageCommitError, ValidationError},
11    messages::group_info::GroupInfo,
12    storage::OpenMlsProvider,
13    tree::sender_ratchet::SenderRatchetConfiguration,
14};
15
16#[cfg(feature = "extensions-draft-08")]
17use crate::{
18    component::{ComponentData, ComponentId},
19    extensions::AppDataDictionary,
20    messages::proposals_in::{ProposalIn, ProposalOrRefIn},
21};
22
23#[cfg(feature = "extensions-draft-08")]
24use std::collections::BTreeMap;
25
26use super::{errors::ProcessMessageError, *};
27
28#[cfg(feature = "extensions-draft-08")]
29/// Keeps the old dictionary as well as the values that are being overwritten
30pub struct AppDataDictionaryUpdater<'a> {
31    old_dict: Option<&'a AppDataDictionary>,
32    new_entries: Option<AppDataUpdates>,
33}
34
35/// A diff of update values that can be provided to [`MlsGroup::process_unverified_message_with_app_data_updates`] or [`CommitBuilder::with_app_data_dictionary_updates`]
36///
37/// [`CommitBuilder::with_app_data_dictionary_updates`]: crate::group::CommitBuilder::with_app_data_dictionary_updates
38#[cfg(feature = "extensions-draft-08")]
39#[derive(Default, Debug)]
40pub struct AppDataUpdates(BTreeMap<ComponentId, Option<Vec<u8>>>);
41
42#[cfg(feature = "extensions-draft-08")]
43impl IntoIterator for AppDataUpdates {
44    type Item = (ComponentId, Option<Vec<u8>>);
45
46    type IntoIter = <BTreeMap<ComponentId, Option<Vec<u8>>> as IntoIterator>::IntoIter;
47
48    fn into_iter(self) -> Self::IntoIter {
49        self.0.into_iter()
50    }
51}
52
53#[cfg(feature = "extensions-draft-08")]
54impl AppDataUpdates {
55    /// Returns the number of changes.
56    pub fn len(&self) -> usize {
57        self.0.len()
58    }
59
60    /// Returns whether there are changes.
61    pub fn is_empty(&self) -> bool {
62        self.0.is_empty()
63    }
64}
65
66#[cfg(feature = "extensions-draft-08")]
67impl<'a> AppDataDictionaryUpdater<'a> {
68    /// Creates a new [`AppDataDictionaryUpdater`].
69    pub fn new(old_dict: Option<&'a AppDataDictionary>) -> Self {
70        Self {
71            old_dict,
72            new_entries: None,
73        }
74    }
75
76    /// Looks up the old value for a component.
77    pub fn old_value(&self, component_id: ComponentId) -> Option<&[u8]> {
78        self.old_dict?.get(&component_id)
79    }
80
81    /// Helper method that returns a mutable reference to the
82    /// [`AppDataUpdates`], creating the struct if it does not exist.
83    fn new_entries_mut(&mut self) -> &mut AppDataUpdates {
84        self.new_entries
85            .get_or_insert_with(|| AppDataUpdates(BTreeMap::new()))
86    }
87
88    /// Sets a value in the new_entries. if we already have data for that component id, overwrite
89    /// it. Else add it in the right position.
90    pub fn set(&mut self, component_data: ComponentData) {
91        let (id, data) = component_data.into_parts();
92
93        self.new_entries_mut().0.insert(id, Some(data.into()));
94    }
95
96    /// Flags an entry in the dictionary for removal
97    pub fn remove(&mut self, id: &ComponentId) {
98        self.new_entries_mut().0.insert(*id, None);
99    }
100
101    /// Consumes the updater and returns just the changes, so we can pass them into
102    /// process_unverified_message.
103    /// Only returns Some if we actually called set.
104    pub fn changes(self) -> Option<AppDataUpdates> {
105        self.new_entries
106    }
107}
108
109impl MlsGroup {
110    /// Parses incoming messages from the DS. Checks for syntactic errors and
111    /// makes some semantic checks as well. If the input is an encrypted
112    /// message, it will be decrypted. This processing function does syntactic
113    /// and semantic validation of the message. It returns a [ProcessedMessage]
114    /// enum.
115    ///
116    /// # Errors:
117    /// Returns an [`ProcessMessageError`] when the validation checks fail
118    /// with the exact reason of the failure.
119    pub fn process_message<Provider: OpenMlsProvider>(
120        &mut self,
121        provider: &Provider,
122        message: impl Into<ProtocolMessage>,
123    ) -> Result<ProcessedMessage, ProcessMessageError<Provider::StorageError>> {
124        let unverified_message = self.unprotect_message(provider, message)?;
125
126        // Check if the commit contains AppDataUpdate proposals - if so, the caller
127        // must use process_unverified_message_with_app_data_updates instead
128        #[cfg(feature = "extensions-draft-08")]
129        if let Some(proposals) = unverified_message.committed_proposals() {
130            for proposal_or_ref in proposals {
131                if let ProposalOrRefIn::Proposal(proposal) = proposal_or_ref {
132                    if matches!(proposal.as_ref(), ProposalIn::AppDataUpdate(_)) {
133                        return Err(ProcessMessageError::FoundAppDataUpdateProposal);
134                    }
135                }
136            }
137        }
138        self.process_unverified_message(provider, unverified_message)
139    }
140
141    #[cfg(feature = "extensions-draft-08")]
142    /// Returns a new helper struct for updating the app data
143    pub fn app_data_dictionary_updater<'a>(&'a self) -> AppDataDictionaryUpdater<'a> {
144        AppDataDictionaryUpdater::new(self.context().app_data_dict())
145    }
146
147    /// Parses and deprotects incoming messages from the DS. Checks for syntactic errors, but only
148    /// performs limited semantic checks.
149    pub fn unprotect_message<Provider: OpenMlsProvider>(
150        &mut self,
151        provider: &Provider,
152        message: impl Into<ProtocolMessage>,
153    ) -> Result<UnverifiedMessage, ProcessMessageError<Provider::StorageError>> {
154        // Make sure we are still a member of the group
155        if !self.is_active() {
156            return Err(ProcessMessageError::GroupStateError(
157                MlsGroupStateError::UseAfterEviction,
158            ));
159        }
160        let message = message.into();
161
162        // Check that handshake messages are compatible with the incoming wire format policy
163        if !message.is_external()
164            && message.is_handshake_message()
165            && !self
166                .configuration()
167                .wire_format_policy()
168                .incoming()
169                .is_compatible_with(message.wire_format())
170        {
171            return Err(ProcessMessageError::IncompatibleWireFormat);
172        }
173
174        // Parse the message
175        let sender_ratchet_configuration = *self.configuration().sender_ratchet_configuration();
176
177        // Check if this message will modify the secret tree when decrypting a
178        // private message
179        let will_modify_secret_tree = matches!(message, ProtocolMessage::PrivateMessage(_));
180
181        // Checks the following semantic validation:
182        //  - ValSem002
183        //  - ValSem003
184        //  - ValSem006
185        //  - ValSem007 MembershipTag presence
186        let decrypted_message =
187            self.decrypt_message(provider.crypto(), message, &sender_ratchet_configuration)?;
188
189        // Persist the secret tree if it was modified to ensure forward secrecy
190        if will_modify_secret_tree {
191            provider
192                .storage()
193                .write_message_secrets(self.group_id(), &self.message_secrets_store)
194                .map_err(ProcessMessageError::StorageError)?;
195        }
196
197        let unverified_message = self
198            .public_group
199            .parse_message(decrypted_message, &self.message_secrets_store)
200            .map_err(ProcessMessageError::from)?;
201
202        Ok(unverified_message)
203    }
204
205    /// Stores a standalone proposal in the internal [ProposalStore]
206    pub fn store_pending_proposal<Storage: StorageProvider>(
207        &mut self,
208        storage: &Storage,
209        proposal: QueuedProposal,
210    ) -> Result<(), Storage::Error> {
211        storage.queue_proposal(self.group_id(), &proposal.proposal_reference(), &proposal)?;
212        // Store the proposal in in the internal ProposalStore
213        self.proposal_store_mut().add(proposal);
214
215        Ok(())
216    }
217
218    /// Returns true if there are pending proposals queued in the proposal store.
219    pub fn has_pending_proposals(&self) -> bool {
220        !self.proposal_store().is_empty()
221    }
222
223    /// Creates a Commit message that covers the pending proposals that are
224    /// currently stored in the group's [ProposalStore]. The Commit message is
225    /// created even if there are no valid pending proposals.
226    ///
227    /// Returns an error if there is a pending commit. Otherwise it returns a
228    /// tuple of `Commit, Option<Welcome>, Option<GroupInfo>`, where `Commit`
229    /// and [`Welcome`] are MlsMessages of the type [`MlsMessageOut`].
230    ///
231    /// [`Welcome`]: crate::messages::Welcome
232    // FIXME: #1217
233    #[allow(clippy::type_complexity)]
234    pub fn commit_to_pending_proposals<Provider: OpenMlsProvider>(
235        &mut self,
236        provider: &Provider,
237        signer: &impl Signer,
238    ) -> Result<
239        (MlsMessageOut, Option<MlsMessageOut>, Option<GroupInfo>),
240        CommitToPendingProposalsError<Provider::StorageError>,
241    > {
242        self.is_operational()?;
243
244        // Build and stage the commit using the commit builder
245        // TODO #751
246        let (commit, welcome, group_info) = self
247            .commit_builder()
248            // This forces committing to the proposals in the proposal store:
249            .consume_proposal_store(true)
250            .load_psks(provider.storage())?
251            .build(provider.rand(), provider.crypto(), signer, |_| true)?
252            .stage_commit(provider)?
253            .into_contents();
254
255        Ok((
256            commit,
257            // Turn the [`Welcome`] to an [`MlsMessageOut`], if there is one
258            welcome.map(|welcome| MlsMessageOut::from_welcome(welcome, self.version())),
259            group_info,
260        ))
261    }
262
263    /// Merge a [StagedCommit] into the group after inspection. As this advances
264    /// the epoch of the group, it also clears any pending commits.
265    pub fn merge_staged_commit<Provider: OpenMlsProvider>(
266        &mut self,
267        provider: &Provider,
268        staged_commit: StagedCommit,
269    ) -> Result<(), MergeCommitError<Provider::StorageError>> {
270        // Check if we were removed from the group
271        if staged_commit.self_removed() {
272            self.group_state = MlsGroupState::Inactive;
273        }
274        provider
275            .storage()
276            .write_group_state(self.group_id(), &self.group_state)
277            .map_err(MergeCommitError::StorageError)?;
278
279        // Merge staged commit
280        self.merge_commit(provider, staged_commit)?;
281
282        // Extract and store the resumption psk for the current epoch
283        let resumption_psk = self.group_epoch_secrets().resumption_psk();
284        self.resumption_psk_store
285            .add(self.context().epoch(), resumption_psk.clone());
286        provider
287            .storage()
288            .write_resumption_psk_store(self.group_id(), &self.resumption_psk_store)
289            .map_err(MergeCommitError::StorageError)?;
290
291        // Delete own KeyPackageBundles
292        self.own_leaf_nodes.clear();
293        provider
294            .storage()
295            .delete_own_leaf_nodes(self.group_id())
296            .map_err(MergeCommitError::StorageError)?;
297
298        // Delete a potential pending commit
299        self.clear_pending_commit(provider.storage())
300            .map_err(MergeCommitError::StorageError)?;
301
302        Ok(())
303    }
304
305    /// Merges the pending [`StagedCommit`] if there is one, and
306    /// clears the field by setting it to `None`.
307    pub fn merge_pending_commit<Provider: OpenMlsProvider>(
308        &mut self,
309        provider: &Provider,
310    ) -> Result<(), MergePendingCommitError<Provider::StorageError>> {
311        match &self.group_state {
312            MlsGroupState::PendingCommit(_) => {
313                let old_state = mem::replace(&mut self.group_state, MlsGroupState::Operational);
314                if let MlsGroupState::PendingCommit(pending_commit_state) = old_state {
315                    self.merge_staged_commit(provider, (*pending_commit_state).into())?;
316                }
317                Ok(())
318            }
319            MlsGroupState::Inactive => Err(MlsGroupStateError::UseAfterEviction)?,
320            MlsGroupState::Operational => Ok(()),
321        }
322    }
323
324    /// Helper function to read decryption keypairs.
325    pub(super) fn read_decryption_keypairs(
326        &self,
327        provider: &impl OpenMlsProvider,
328        own_leaf_nodes: &[LeafNode],
329    ) -> Result<(Vec<EncryptionKeyPair>, Vec<EncryptionKeyPair>), StageCommitError> {
330        // All keys from the previous epoch are potential decryption keypairs.
331        let old_epoch_keypairs = self.read_epoch_keypairs(provider.storage()).map_err(|e| {
332            log::error!("Error reading epoch keypairs: {e:?}");
333            StageCommitError::MissingDecryptionKey
334        })?;
335
336        // If we are processing an update proposal that originally came from
337        // us, the keypair corresponding to the leaf in the update is also a
338        // potential decryption keypair.
339        let leaf_node_keypairs = own_leaf_nodes
340            .iter()
341            .map(|leaf_node| {
342                EncryptionKeyPair::read(provider, leaf_node.encryption_key())
343                    .ok_or(StageCommitError::MissingDecryptionKey)
344            })
345            .collect::<Result<Vec<EncryptionKeyPair>, StageCommitError>>()?;
346
347        Ok((old_epoch_keypairs, leaf_node_keypairs))
348    }
349
350    /// This function processes a message that may contain AppDataUpdate proposals.
351    /// Process these first an create an AppDataUpdates, then pass that into this function.
352    #[cfg(feature = "extensions-draft-08")]
353    pub fn process_unverified_message_with_app_data_updates<Provider: OpenMlsProvider>(
354        &self,
355        provider: &Provider,
356        unverified_message: UnverifiedMessage,
357        app_data_dict_updates: Option<AppDataUpdates>,
358    ) -> Result<ProcessedMessage, ProcessMessageError<Provider::StorageError>> {
359        // Checks the following semantic validation:
360        //  - ValSem010
361        //  - ValSem246 (as part of ValSem010)
362        //  - https://validation.openmls.tech/#valn1302
363        //  - https://validation.openmls.tech/#valn1304
364        let (content, credential) =
365            unverified_message.verify(self.ciphersuite(), provider.crypto(), self.version())?;
366
367        #[cfg_attr(not(feature = "extensions-draft-08"), allow(unused_mut))]
368        let mut processed = match content.sender() {
369            Sender::Member(_) | Sender::NewMemberProposal | Sender::NewMemberCommit => self
370                .process_internal_authenticated_content_with_app_data_updates(
371                    provider,
372                    content,
373                    credential,
374                    app_data_dict_updates,
375                )?,
376            Sender::External(_) => {
377                self.process_external_authenticated_content(provider, content, credential)?
378            }
379        };
380        #[cfg(feature = "extensions-draft-08")]
381        if self.context().safe_aad_required() {
382            processed
383                .try_attach_safe_aad()
384                .map_err(|_| ProcessMessageError::MalformedSafeAad)?;
385        }
386        Ok(processed)
387    }
388
389    /// This processing function does most of the semantic verifications.
390    /// It returns a [ProcessedMessage] enum.
391    ///
392    /// Note: If you expect `AppDataUpdate` proposals, use
393    /// `process_unverified_message_with_app_data_updates` instead!
394    ///
395    /// Checks the following semantic validation:
396    ///  - ValSem008
397    ///  - ValSem010
398    ///  - ValSem101
399    ///  - ValSem102
400    ///  - ValSem104
401    ///  - ValSem106
402    ///  - ValSem107
403    ///  - ValSem108
404    ///  - ValSem110
405    ///  - ValSem111
406    ///  - ValSem112
407    ///  - ValSem113: All Proposals: The proposal type must be supported by all
408    ///    members of the group
409    ///  - ValSem200
410    ///  - ValSem201
411    ///  - ValSem202: Path must be the right length
412    ///  - ValSem203: Path secrets must decrypt correctly
413    ///  - ValSem204: Public keys from Path must be verified and match the
414    ///    private keys from the direct path
415    ///  - ValSem205
416    pub(crate) fn process_unverified_message<Provider: OpenMlsProvider>(
417        &self,
418        provider: &Provider,
419        unverified_message: UnverifiedMessage,
420    ) -> Result<ProcessedMessage, ProcessMessageError<Provider::StorageError>> {
421        // Checks the following semantic validation:
422        //  - ValSem010
423        //  - ValSem246 (as part of ValSem010)
424        //  - https://validation.openmls.tech/#valn1302
425        //  - https://validation.openmls.tech/#valn1304
426        let (content, credential) =
427            unverified_message.verify(self.ciphersuite(), provider.crypto(), self.version())?;
428
429        #[cfg_attr(not(feature = "extensions-draft-08"), allow(unused_mut))]
430        let mut processed = match content.sender() {
431            Sender::Member(_) | Sender::NewMemberProposal | Sender::NewMemberCommit => {
432                self.process_internal_authenticated_content(provider, content, credential)?
433            }
434            Sender::External(_) => {
435                self.process_external_authenticated_content(provider, content, credential)?
436            }
437        };
438        #[cfg(feature = "extensions-draft-08")]
439        if self.context().safe_aad_required() {
440            processed
441                .try_attach_safe_aad()
442                .map_err(|_| ProcessMessageError::MalformedSafeAad)?;
443        }
444        Ok(processed)
445    }
446
447    /// This processing function does most of the semantic verifications.
448    /// It returns a [ProcessedMessage] enum.
449    /// Checks the following semantic validation:
450    ///  - ValSem008
451    ///  - ValSem010
452    ///  - ValSem101
453    ///  - ValSem102
454    ///  - ValSem104
455    ///  - ValSem106
456    ///  - ValSem107
457    ///  - ValSem108
458    ///  - ValSem110
459    ///  - ValSem111
460    ///  - ValSem112
461    ///  - ValSem113: All Proposals: The proposal type must be supported by all
462    ///    members of the group
463    ///  - ValSem200
464    ///  - ValSem201
465    ///  - ValSem202: Path must be the right length
466    ///  - ValSem203: Path secrets must decrypt correctly
467    ///  - ValSem204: Public keys from Path must be verified and match the
468    ///    private keys from the direct path
469    ///  - ValSem205
470    #[cfg(feature = "extensions-draft-08")]
471    fn process_internal_authenticated_content_with_app_data_updates<Provider: OpenMlsProvider>(
472        &self,
473        provider: &Provider,
474        content: AuthenticatedContent,
475        credential: Credential,
476        app_data_dict_updates: Option<AppDataUpdates>,
477    ) -> Result<ProcessedMessage, ProcessMessageError<Provider::StorageError>> {
478        let sender = content.sender().clone();
479        let authenticated_data = content.authenticated_data().to_owned();
480        let epoch = content.epoch();
481
482        let content = match content.content() {
483            FramedContentBody::Application(application_message) => {
484                ProcessedMessageContent::ApplicationMessage(ApplicationMessage::new(
485                    application_message.as_slice().to_owned(),
486                ))
487            }
488            FramedContentBody::Proposal(_) => {
489                let proposal = Box::new(QueuedProposal::from_authenticated_content_by_ref(
490                    self.ciphersuite(),
491                    provider.crypto(),
492                    content,
493                )?);
494
495                if matches!(sender, Sender::NewMemberProposal) {
496                    ProcessedMessageContent::ExternalJoinProposalMessage(proposal)
497                } else {
498                    ProcessedMessageContent::ProposalMessage(proposal)
499                }
500            }
501            FramedContentBody::Commit(_) => {
502                // Since this is a commit, we need to load the private key material we need for decryption.
503                let (old_epoch_keypairs, leaf_node_keypairs) =
504                    self.read_decryption_keypairs(provider, &self.own_leaf_nodes)?;
505
506                let staged_commit = self.stage_commit_with_app_data_updates(
507                    &content,
508                    old_epoch_keypairs,
509                    leaf_node_keypairs,
510                    app_data_dict_updates,
511                    provider,
512                )?;
513
514                ProcessedMessageContent::StagedCommitMessage(Box::new(staged_commit))
515            }
516        };
517
518        Ok(ProcessedMessage::new(
519            self.group_id().clone(),
520            epoch,
521            sender,
522            authenticated_data,
523            content,
524            credential,
525        ))
526    }
527
528    fn process_internal_authenticated_content<Provider: OpenMlsProvider>(
529        &self,
530        provider: &Provider,
531        content: AuthenticatedContent,
532        credential: Credential,
533    ) -> Result<ProcessedMessage, ProcessMessageError<Provider::StorageError>> {
534        let sender = content.sender().clone();
535        let authenticated_data = content.authenticated_data().to_owned();
536        let epoch = content.epoch();
537
538        let content = match content.content() {
539            FramedContentBody::Application(application_message) => {
540                ProcessedMessageContent::ApplicationMessage(ApplicationMessage::new(
541                    application_message.as_slice().to_owned(),
542                ))
543            }
544            FramedContentBody::Proposal(_) => {
545                let proposal = Box::new(QueuedProposal::from_authenticated_content_by_ref(
546                    self.ciphersuite(),
547                    provider.crypto(),
548                    content,
549                )?);
550
551                if matches!(sender, Sender::NewMemberProposal) {
552                    ProcessedMessageContent::ExternalJoinProposalMessage(proposal)
553                } else {
554                    ProcessedMessageContent::ProposalMessage(proposal)
555                }
556            }
557            FramedContentBody::Commit(_) => {
558                // Since this is a commit, we need to load the private key material we need for decryption.
559                let (old_epoch_keypairs, leaf_node_keypairs) =
560                    self.read_decryption_keypairs(provider, &self.own_leaf_nodes)?;
561
562                let staged_commit =
563                    self.stage_commit(&content, old_epoch_keypairs, leaf_node_keypairs, provider)?;
564
565                ProcessedMessageContent::StagedCommitMessage(Box::new(staged_commit))
566            }
567        };
568
569        Ok(ProcessedMessage::new(
570            self.group_id().clone(),
571            epoch,
572            sender,
573            authenticated_data,
574            content,
575            credential,
576        ))
577    }
578
579    ///  - ValSem240
580    ///  - ValSem241
581    ///  - ValSem242
582    ///  - ValSem244
583    ///  - ValSem246 (as part of ValSem010)
584    fn process_external_authenticated_content<Provider: OpenMlsProvider>(
585        &self,
586        provider: &Provider,
587        content: AuthenticatedContent,
588        credential: Credential,
589    ) -> Result<ProcessedMessage, ProcessMessageError<Provider::StorageError>> {
590        let sender = content.sender().clone();
591        let data = content.authenticated_data().to_owned();
592
593        debug_assert!(matches!(sender, Sender::External(_)));
594
595        // https://validation.openmls.tech/#valn1501
596        match content.content() {
597            FramedContentBody::Application(_) => {
598                Err(ProcessMessageError::UnauthorizedExternalApplicationMessage)
599            }
600            // TODO: https://validation.openmls.tech/#valn1502
601            FramedContentBody::Proposal(Proposal::GroupContextExtensions(_)) => {
602                let content = ProcessedMessageContent::ProposalMessage(Box::new(
603                    QueuedProposal::from_authenticated_content_by_ref(
604                        self.ciphersuite(),
605                        provider.crypto(),
606                        content,
607                    )?,
608                ));
609                Ok(ProcessedMessage::new(
610                    self.group_id().clone(),
611                    self.context().epoch(),
612                    sender,
613                    data,
614                    content,
615                    credential,
616                ))
617            }
618
619            FramedContentBody::Proposal(Proposal::Remove(_)) => {
620                let content = ProcessedMessageContent::ProposalMessage(Box::new(
621                    QueuedProposal::from_authenticated_content_by_ref(
622                        self.ciphersuite(),
623                        provider.crypto(),
624                        content,
625                    )?,
626                ));
627                Ok(ProcessedMessage::new(
628                    self.group_id().clone(),
629                    self.context().epoch(),
630                    sender,
631                    data,
632                    content,
633                    credential,
634                ))
635            }
636            FramedContentBody::Proposal(Proposal::Add(_)) => {
637                let content = ProcessedMessageContent::ProposalMessage(Box::new(
638                    QueuedProposal::from_authenticated_content_by_ref(
639                        self.ciphersuite(),
640                        provider.crypto(),
641                        content,
642                    )?,
643                ));
644                Ok(ProcessedMessage::new(
645                    self.group_id().clone(),
646                    self.context().epoch(),
647                    sender,
648                    data,
649                    content,
650                    credential,
651                ))
652            }
653            // TODO #151/#106
654            FramedContentBody::Proposal(_) => Err(ProcessMessageError::UnsupportedProposalType),
655            FramedContentBody::Commit(_) => {
656                Err(ProcessMessageError::UnauthorizedExternalCommitMessage)
657            }
658        }
659    }
660
661    /// Performs framing validation and, if necessary, decrypts the given message.
662    ///
663    /// Returns the [`DecryptedMessage`] if processing is successful, or a
664    /// [`ValidationError`] if it is not.
665    ///
666    /// Checks the following semantic validation:
667    ///  - ValSem002
668    ///  - ValSem003
669    ///  - ValSem006
670    ///  - ValSem007 MembershipTag presence
671    ///  - https://validation.openmls.tech/#valn1202
672    pub(crate) fn decrypt_message(
673        &mut self,
674        crypto: &impl OpenMlsCrypto,
675        message: ProtocolMessage,
676        sender_ratchet_configuration: &SenderRatchetConfiguration,
677    ) -> Result<DecryptedMessage, ValidationError> {
678        // Checks the following semantic validation:
679        //  - ValSem002
680        //  - ValSem003
681        self.public_group.validate_framing(&message)?;
682
683        let epoch = message.epoch();
684
685        // Checks the following semantic validation:
686        //  - ValSem006
687        //  - ValSem007 MembershipTag presence
688        match message {
689            ProtocolMessage::PublicMessage(public_message) => {
690                // If the message is older than the current epoch, we need to fetch the correct secret tree first.
691                let message_secrets =
692                    self.message_secrets_for_epoch(epoch).map_err(|e| match e {
693                        SecretTreeError::TooDistantInThePast => ValidationError::NoPastEpochData,
694                        _ => LibraryError::custom(
695                            "Unexpected error while retrieving message secrets for epoch.",
696                        )
697                        .into(),
698                    })?;
699                DecryptedMessage::from_inbound_public_message(
700                    *public_message,
701                    message_secrets,
702                    message_secrets.serialized_context().to_vec(),
703                    crypto,
704                    self.ciphersuite(),
705                )
706            }
707            ProtocolMessage::PrivateMessage(ciphertext) => {
708                // If the message is older than the current epoch, we need to fetch the correct secret tree first
709                DecryptedMessage::from_inbound_ciphertext(
710                    ciphertext,
711                    crypto,
712                    self,
713                    sender_ratchet_configuration,
714                )
715            }
716        }
717    }
718}