Skip to main content

openmls/group/public_group/
process.rs

1//! This module contains the implementation of the processing functions for
2//! public groups.
3
4use openmls_traits::crypto::OpenMlsCrypto;
5use tls_codec::Serialize;
6
7use crate::{
8    ciphersuite::OpenMlsSignaturePublicKey,
9    credentials::{Credential, CredentialWithKey},
10    error::LibraryError,
11    framing::{
12        mls_auth_content::AuthenticatedContent, mls_content::FramedContentBody, ApplicationMessage,
13        DecryptedMessage, ProcessedMessage, ProcessedMessageContent, ProtocolMessage, Sender,
14        SenderContext, UnverifiedMessage,
15    },
16    group::{
17        errors::ValidationError, past_secrets::MessageSecretsStore, proposal_store::QueuedProposal,
18        PublicProcessMessageError,
19    },
20    messages::proposals::Proposal,
21};
22
23#[cfg(feature = "extensions-draft")]
24use crate::{
25    group::{
26        mls_group::processing::{committed_app_data_update_proposals, UnresolvedAppDataCommit},
27        ResolveAppDataCommitError, StageCommitError, StagedCommit,
28    },
29    prelude::processing::{AppDataDictionaryUpdater, AppDataUpdates},
30};
31
32use super::PublicGroup;
33
34impl PublicGroup {
35    /// This function is used to parse messages from the DS.
36    /// It checks for syntactic errors and makes some semantic checks as well.
37    /// If the input is a [PrivateMessage] message, it will be decrypted.
38    /// Returns an [UnverifiedMessage] that can be inspected and later processed in
39    /// [Self::process_unverified_message()].
40    /// Checks the following semantic validation:
41    ///  - ValSem002
42    ///  - ValSem003
43    ///  - ValSem004
44    ///  - ValSem005
45    ///  - ValSem006
46    ///  - ValSem007
47    ///  - ValSem009
48    ///  - ValSem112
49    ///  - ValSem245
50    pub(crate) fn parse_message<'a>(
51        &self,
52        decrypted_message: DecryptedMessage,
53        message_secrets_store_option: impl Into<Option<&'a MessageSecretsStore>>,
54    ) -> Result<UnverifiedMessage, ValidationError> {
55        let message_secrets_store_option = message_secrets_store_option.into();
56        let verifiable_content = decrypted_message.verifiable_content();
57
58        // Checks the following semantic validation:
59        //  - ValSem004
60        //  - ValSem005
61        //  - ValSem009
62        self.validate_verifiable_content(verifiable_content, message_secrets_store_option)?;
63
64        let message_epoch = verifiable_content.epoch();
65
66        // Depending on the epoch of the message, use the correct set of leaf nodes for getting the
67        // credential and signature key for the member with given index.
68        let look_up_credential_with_key = |leaf_node_index| {
69            if message_epoch == self.group_context().epoch() {
70                self.treesync()
71                    .leaf(leaf_node_index)
72                    .map(CredentialWithKey::from)
73            } else if let Some(store) = message_secrets_store_option {
74                // The message is from a past epoch, look up the member in the
75                // past secrets store based on the epoch and sender's leaf
76                // index.
77                store
78                    .leaves_for_epoch(message_epoch)
79                    .get(&leaf_node_index)
80                    .map(|&member| CredentialWithKey::from(member))
81            } else {
82                None
83            }
84        };
85
86        // Extract the credential if the sender is a member or a new member.
87        // Checks the following semantic validation:
88        //  - ValSem112
89        //  - ValSem245
90        //  - Prepares ValSem246 by setting the right credential. The remainder
91        //    of ValSem246 is validated as part of ValSem010.
92        // External senders are not supported yet #106/#151.
93        let CredentialWithKey {
94            credential,
95            signature_key,
96        } = decrypted_message.credential(
97            look_up_credential_with_key,
98            self.group_context().extensions().external_senders(),
99        )?;
100        let signature_public_key = OpenMlsSignaturePublicKey::from_signature_key(
101            signature_key,
102            self.ciphersuite().signature_algorithm(),
103        );
104
105        // For commit messages, we need to check if the sender is a member or a
106        // new member and set the tree position accordingly.
107        let sender_context = match decrypted_message.sender() {
108            Sender::Member(leaf_index) => Some(SenderContext::Member((
109                self.group_id().clone(),
110                *leaf_index,
111            ))),
112            Sender::NewMemberCommit => Some(SenderContext::ExternalCommit {
113                group_id: self.group_id().clone(),
114                leftmost_blank_index: self.treesync().free_leaf_index(),
115                self_removes_in_store: self.proposal_store.self_removes(),
116            }),
117            Sender::External(_) | Sender::NewMemberProposal => None,
118        };
119
120        Ok(UnverifiedMessage::from_decrypted_message(
121            decrypted_message,
122            credential,
123            signature_public_key,
124            sender_context,
125        ))
126    }
127
128    /// This function is used to parse messages from the DS. It checks for
129    /// syntactic errors and does semantic validation as well. It returns a
130    /// [ProcessedMessage] enum. Checks the following semantic validation:
131    ///  - ValSem002
132    ///  - ValSem003
133    ///  - ValSem004
134    ///  - ValSem005
135    ///  - ValSem006
136    ///  - ValSem007
137    ///  - ValSem008
138    ///  - ValSem009
139    ///  - ValSem010
140    ///  - ValSem101
141    ///  - ValSem102
142    ///  - ValSem104
143    ///  - ValSem106
144    ///  - ValSem107
145    ///  - ValSem108
146    ///  - ValSem110
147    ///  - ValSem111
148    ///  - ValSem112
149    ///  - ValSem200
150    ///  - ValSem201
151    ///  - ValSem202: Path must be the right length
152    ///  - ValSem203: Path secrets must decrypt correctly
153    ///  - ValSem204: Public keys from Path must be verified and match the
154    ///    private keys from the direct path
155    ///  - ValSem205
156    ///  - ValSem240
157    ///  - ValSem241
158    ///  - ValSem242
159    ///  - ValSem244
160    ///  - ValSem245
161    ///  - ValSem246 (as part of ValSem010)
162    ///
163    #[cfg_attr(
164        feature = "extensions-draft",
165        doc = "A commit covering AppDataUpdate proposals is returned as\n\
166        [`ProcessedMessageContent::UnresolvedAppDataCommit`], since the\n\
167        application has to interpret the proposals before the commit can be\n\
168        staged via [`PublicGroup::stage_app_data_commit()`]."
169    )]
170    pub fn process_message(
171        &self,
172        crypto: &impl OpenMlsCrypto,
173        message: impl Into<ProtocolMessage>,
174    ) -> Result<ProcessedMessage, PublicProcessMessageError> {
175        let protocol_message = message.into();
176        // Checks the following semantic validation:
177        //  - ValSem002
178        //  - ValSem003
179        self.validate_framing(&protocol_message)?;
180
181        let decrypted_message = match protocol_message {
182            ProtocolMessage::PrivateMessage(_) => {
183                return Err(PublicProcessMessageError::IncompatibleWireFormat)
184            }
185            ProtocolMessage::PublicMessage(public_message) => {
186                DecryptedMessage::from_inbound_public_message(
187                    *public_message,
188                    None,
189                    self.group_context()
190                        .tls_serialize_detached()
191                        .map_err(LibraryError::missing_bound_check)?,
192                    crypto,
193                    self.ciphersuite(),
194                )?
195            }
196        };
197
198        let unverified_message = self
199            .parse_message(decrypted_message, None)
200            .map_err(PublicProcessMessageError::from)?;
201        self.process_unverified_message(crypto, unverified_message)
202    }
203
204    #[cfg(feature = "extensions-draft")]
205    /// Returns a new helper struct for updating the app data
206    pub fn app_data_dictionary_updater(&self) -> AppDataDictionaryUpdater<'_> {
207        AppDataDictionaryUpdater::new(self.group_context().app_data_dict())
208    }
209
210    /// Stages a Commit covering AppDataUpdate proposals, after the application
211    /// has interpreted the proposals and computed the resulting
212    /// [`AppDataUpdates`].
213    ///
214    /// The returned [`StagedCommit`] can be inspected and merged into the
215    /// group's state using [`PublicGroup::merge_commit()`].
216    #[cfg(feature = "extensions-draft")]
217    pub fn stage_app_data_commit(
218        &self,
219        crypto: &impl OpenMlsCrypto,
220        unresolved_commit: UnresolvedAppDataCommit,
221        app_data_dict_updates: Option<AppDataUpdates>,
222    ) -> Result<StagedCommit, StageCommitError> {
223        self.stage_commit_with_app_data_updates(
224            &unresolved_commit.into_content(),
225            crypto,
226            app_data_dict_updates,
227        )
228    }
229
230    /// Resolves a [`ProcessedMessage`] carrying an
231    /// [`ProcessedMessageContent::UnresolvedAppDataCommit`]: stages the commit
232    /// with the application-computed [`AppDataUpdates`] and returns the same
233    /// message with the resulting [`StagedCommit`] as regular
234    /// [`ProcessedMessageContent::StagedCommitMessage`] content. All other
235    /// message fields (sender, credential, authenticated data) are preserved.
236    ///
237    /// Use this instead of [`PublicGroup::stage_app_data_commit()`] when the
238    /// caller needs the resolved commit in [`ProcessedMessage`] form, e.g. to
239    /// keep a single code path for commits with and without AppDataUpdate
240    /// proposals.
241    ///
242    /// Returns an error if the message content is not an unresolved app data
243    /// commit; the message is consumed either way.
244    #[cfg(feature = "extensions-draft")]
245    pub fn resolve_app_data_commit(
246        &self,
247        crypto: &impl OpenMlsCrypto,
248        processed_message: ProcessedMessage,
249        app_data_dict_updates: Option<AppDataUpdates>,
250    ) -> Result<ProcessedMessage, ResolveAppDataCommitError> {
251        processed_message.resolve_app_data_commit(|unresolved_commit| {
252            self.stage_app_data_commit(crypto, unresolved_commit, app_data_dict_updates)
253        })
254    }
255}
256
257impl PublicGroup {
258    /// This processing function does most of the semantic verifications.
259    /// It returns a [ProcessedMessage] enum.
260    /// Checks the following semantic validation:
261    ///  - ValSem008
262    ///  - ValSem010
263    ///  - ValSem101
264    ///  - ValSem102
265    ///  - ValSem104
266    ///  - ValSem106
267    ///  - ValSem107
268    ///  - ValSem108
269    ///  - ValSem110
270    ///  - ValSem111
271    ///  - ValSem112
272    ///  - ValSem200
273    ///  - ValSem201
274    ///  - ValSem202: Path must be the right length
275    ///  - ValSem203: Path secrets must decrypt correctly
276    ///  - ValSem204: Public keys from Path must be verified and match the
277    ///    private keys from the direct path
278    ///  - ValSem205
279    ///  - ValSem240
280    ///  - ValSem241
281    ///  - ValSem242
282    ///  - ValSem244
283    ///  - ValSem246 (as part of ValSem010)
284    pub(crate) fn process_unverified_message(
285        &self,
286        crypto: &impl OpenMlsCrypto,
287        unverified_message: UnverifiedMessage,
288    ) -> Result<ProcessedMessage, PublicProcessMessageError> {
289        // Checks the following semantic validation:
290        //  - ValSem010
291        //  - ValSem246 (as part of ValSem010)
292        //  - https://validation.openmls.tech/#valn1203
293        let verified = unverified_message.verify(self.ciphersuite(), crypto, self.version())?;
294        let content = verified.content;
295        let credential = verified.credential;
296
297        #[cfg_attr(not(feature = "extensions-draft"), allow(unused_mut))]
298        let mut processed = match content.sender() {
299            Sender::Member(_) | Sender::NewMemberCommit | Sender::NewMemberProposal => {
300                self.process_internal_authenticated_content(crypto, content, credential)?
301            }
302            Sender::External(_) => {
303                self.process_external_authenticated_content(crypto, content, credential)?
304            }
305        };
306        #[cfg(feature = "extensions-draft")]
307        if self.group_context().safe_aad_required() {
308            processed
309                .try_attach_safe_aad()
310                .map_err(|_| PublicProcessMessageError::MalformedSafeAad)?;
311        }
312        Ok(processed)
313    }
314
315    fn process_internal_authenticated_content(
316        &self,
317        crypto: &impl OpenMlsCrypto,
318        content: AuthenticatedContent,
319        credential: Credential,
320    ) -> Result<ProcessedMessage, PublicProcessMessageError> {
321        let sender = content.sender().clone();
322        let authenticated_data = content.authenticated_data().to_owned();
323
324        let content = match content.content() {
325            FramedContentBody::Application(application_message) => {
326                ProcessedMessageContent::ApplicationMessage(ApplicationMessage::new(
327                    application_message.as_slice().to_owned(),
328                ))
329            }
330            FramedContentBody::Proposal(_) => {
331                let proposal = Box::new(QueuedProposal::from_authenticated_content_by_ref(
332                    self.ciphersuite(),
333                    crypto,
334                    content,
335                )?);
336                if matches!(sender, Sender::NewMemberProposal) {
337                    ProcessedMessageContent::ExternalJoinProposalMessage(proposal)
338                } else {
339                    ProcessedMessageContent::ProposalMessage(proposal)
340                }
341            }
342            FramedContentBody::Commit(commit) => {
343                // A commit covering AppDataUpdate proposals cannot be staged
344                // immediately: the proposals contain diffs in an
345                // application-defined format, so the application has to
346                // interpret them and supply the resulting dictionary entries
347                // first. The verified content is handed back to the caller,
348                // who resumes staging via `PublicGroup::stage_app_data_commit`.
349                #[cfg(feature = "extensions-draft")]
350                {
351                    let app_data_update_proposals =
352                        committed_app_data_update_proposals(commit, &self.proposal_store);
353                    if !app_data_update_proposals.is_empty() {
354                        let unresolved_commit =
355                            UnresolvedAppDataCommit::new(content, app_data_update_proposals);
356                        return Ok(ProcessedMessage::new(
357                            self.group_id().clone(),
358                            self.group_context().epoch(),
359                            sender,
360                            authenticated_data,
361                            ProcessedMessageContent::UnresolvedAppDataCommit(Box::new(
362                                unresolved_commit,
363                            )),
364                            credential,
365                            #[cfg(feature = "virtual-clients-draft")]
366                            None,
367                        ));
368                    }
369                }
370                #[cfg(not(feature = "extensions-draft"))]
371                let _ = commit;
372
373                let staged_commit = self.stage_commit(&content, crypto)?;
374                ProcessedMessageContent::StagedCommitMessage(Box::new(staged_commit))
375            }
376        };
377
378        Ok(ProcessedMessage::new(
379            self.group_id().clone(),
380            self.group_context().epoch(),
381            sender,
382            authenticated_data,
383            content,
384            credential,
385            #[cfg(feature = "virtual-clients-draft")]
386            None,
387        ))
388    }
389
390    fn process_external_authenticated_content(
391        &self,
392        crypto: &impl OpenMlsCrypto,
393        content: AuthenticatedContent,
394        credential: Credential,
395    ) -> Result<ProcessedMessage, PublicProcessMessageError> {
396        let sender = content.sender().clone();
397        let data = content.authenticated_data().to_owned();
398
399        debug_assert!(matches!(sender, Sender::External(_)));
400
401        // https://validation.openmls.tech/#valn1501
402        match content.content() {
403            FramedContentBody::Application(_) => {
404                Err(PublicProcessMessageError::UnauthorizedExternalApplicationMessage)
405            }
406            // TODO: https://validation.openmls.tech/#valn1502
407            FramedContentBody::Proposal(Proposal::GroupContextExtensions(_)) => {
408                let content = ProcessedMessageContent::ProposalMessage(Box::new(
409                    QueuedProposal::from_authenticated_content_by_ref(
410                        self.ciphersuite(),
411                        crypto,
412                        content,
413                    )?,
414                ));
415                Ok(ProcessedMessage::new(
416                    self.group_id().clone(),
417                    self.group_context().epoch(),
418                    sender,
419                    data,
420                    content,
421                    credential,
422                    #[cfg(feature = "virtual-clients-draft")]
423                    None,
424                ))
425            }
426
427            FramedContentBody::Proposal(Proposal::Remove(_)) => {
428                let content = ProcessedMessageContent::ProposalMessage(Box::new(
429                    QueuedProposal::from_authenticated_content_by_ref(
430                        self.ciphersuite(),
431                        crypto,
432                        content,
433                    )?,
434                ));
435                Ok(ProcessedMessage::new(
436                    self.group_id().clone(),
437                    self.group_context().epoch(),
438                    sender,
439                    data,
440                    content,
441                    credential,
442                    #[cfg(feature = "virtual-clients-draft")]
443                    None,
444                ))
445            }
446            FramedContentBody::Proposal(Proposal::Add(_)) => {
447                let content = ProcessedMessageContent::ProposalMessage(Box::new(
448                    QueuedProposal::from_authenticated_content_by_ref(
449                        self.ciphersuite(),
450                        crypto,
451                        content,
452                    )?,
453                ));
454                Ok(ProcessedMessage::new(
455                    self.group_id().clone(),
456                    self.group_context().epoch(),
457                    sender,
458                    data,
459                    content,
460                    credential,
461                    #[cfg(feature = "virtual-clients-draft")]
462                    None,
463                ))
464            }
465            // TODO #151/#106
466            FramedContentBody::Proposal(_) => {
467                Err(PublicProcessMessageError::UnsupportedProposalType)
468            }
469            FramedContentBody::Commit(_) => {
470                Err(PublicProcessMessageError::UnauthorizedExternalCommitMessage)
471            }
472        }
473    }
474}