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-08")]
24use crate::{
25    component::ComponentData,
26    messages::{
27        proposals::AppDataUpdateOperation,
28        proposals_in::{ProposalIn, ProposalOrRefIn},
29    },
30    prelude::processing::{AppDataDictionaryUpdater, AppDataUpdates},
31};
32
33use super::PublicGroup;
34
35impl PublicGroup {
36    /// This function is used to parse messages from the DS.
37    /// It checks for syntactic errors and makes some semantic checks as well.
38    /// If the input is a [PrivateMessage] message, it will be decrypted.
39    /// Returns an [UnverifiedMessage] that can be inspected and later processed in
40    /// [Self::process_unverified_message()].
41    /// Checks the following semantic validation:
42    ///  - ValSem002
43    ///  - ValSem003
44    ///  - ValSem004
45    ///  - ValSem005
46    ///  - ValSem006
47    ///  - ValSem007
48    ///  - ValSem009
49    ///  - ValSem112
50    ///  - ValSem245
51    pub(crate) fn parse_message<'a>(
52        &self,
53        decrypted_message: DecryptedMessage,
54        message_secrets_store_option: impl Into<Option<&'a MessageSecretsStore>>,
55    ) -> Result<UnverifiedMessage, ValidationError> {
56        let message_secrets_store_option = message_secrets_store_option.into();
57        let verifiable_content = decrypted_message.verifiable_content();
58
59        // Checks the following semantic validation:
60        //  - ValSem004
61        //  - ValSem005
62        //  - ValSem009
63        self.validate_verifiable_content(verifiable_content, message_secrets_store_option)?;
64
65        let message_epoch = verifiable_content.epoch();
66
67        // Depending on the epoch of the message, use the correct set of leaf nodes for getting the
68        // credential and signature key for the member with given index.
69        let look_up_credential_with_key = |leaf_node_index| {
70            if message_epoch == self.group_context().epoch() {
71                self.treesync()
72                    .leaf(leaf_node_index)
73                    .map(CredentialWithKey::from)
74            } else if let Some(store) = message_secrets_store_option {
75                // The message is from a past epoch, look up the member in the
76                // past secrets store based on the epoch and sender's leaf
77                // index.
78                store
79                    .leaves_for_epoch(message_epoch)
80                    .get(&leaf_node_index)
81                    .map(|&member| CredentialWithKey::from(member))
82            } else {
83                None
84            }
85        };
86
87        // Extract the credential if the sender is a member or a new member.
88        // Checks the following semantic validation:
89        //  - ValSem112
90        //  - ValSem245
91        //  - Prepares ValSem246 by setting the right credential. The remainder
92        //    of ValSem246 is validated as part of ValSem010.
93        // External senders are not supported yet #106/#151.
94        let CredentialWithKey {
95            credential,
96            signature_key,
97        } = decrypted_message.credential(
98            look_up_credential_with_key,
99            self.group_context().extensions().external_senders(),
100        )?;
101        let signature_public_key = OpenMlsSignaturePublicKey::from_signature_key(
102            signature_key,
103            self.ciphersuite().signature_algorithm(),
104        );
105
106        // For commit messages, we need to check if the sender is a member or a
107        // new member and set the tree position accordingly.
108        let sender_context = match decrypted_message.sender() {
109            Sender::Member(leaf_index) => Some(SenderContext::Member((
110                self.group_id().clone(),
111                *leaf_index,
112            ))),
113            Sender::NewMemberCommit => Some(SenderContext::ExternalCommit {
114                group_id: self.group_id().clone(),
115                leftmost_blank_index: self.treesync().free_leaf_index(),
116                self_removes_in_store: self.proposal_store.self_removes(),
117            }),
118            Sender::External(_) | Sender::NewMemberProposal => None,
119        };
120
121        Ok(UnverifiedMessage::from_decrypted_message(
122            decrypted_message,
123            credential,
124            signature_public_key,
125            sender_context,
126        ))
127    }
128
129    /// This function is used to parse messages from the DS. It checks for
130    /// syntactic errors and does semantic validation as well. It returns a
131    /// [ProcessedMessage] enum. Checks the following semantic validation:
132    ///  - ValSem002
133    ///  - ValSem003
134    ///  - ValSem004
135    ///  - ValSem005
136    ///  - ValSem006
137    ///  - ValSem007
138    ///  - ValSem008
139    ///  - ValSem009
140    ///  - ValSem010
141    ///  - ValSem101
142    ///  - ValSem102
143    ///  - ValSem104
144    ///  - ValSem106
145    ///  - ValSem107
146    ///  - ValSem108
147    ///  - ValSem110
148    ///  - ValSem111
149    ///  - ValSem112
150    ///  - ValSem200
151    ///  - ValSem201
152    ///  - ValSem202: Path must be the right length
153    ///  - ValSem203: Path secrets must decrypt correctly
154    ///  - ValSem204: Public keys from Path must be verified and match the
155    ///    private keys from the direct path
156    ///  - ValSem205
157    ///  - ValSem240
158    ///  - ValSem241
159    ///  - ValSem242
160    ///  - ValSem244
161    ///  - ValSem245
162    ///  - ValSem246 (as part of ValSem010)
163    pub fn process_message(
164        &self,
165        crypto: &impl OpenMlsCrypto,
166        message: impl Into<ProtocolMessage>,
167    ) -> Result<ProcessedMessage, PublicProcessMessageError> {
168        let protocol_message = message.into();
169        // Checks the following semantic validation:
170        //  - ValSem002
171        //  - ValSem003
172        self.validate_framing(&protocol_message)?;
173
174        let decrypted_message = match protocol_message {
175            ProtocolMessage::PrivateMessage(_) => {
176                return Err(PublicProcessMessageError::IncompatibleWireFormat)
177            }
178            ProtocolMessage::PublicMessage(public_message) => {
179                DecryptedMessage::from_inbound_public_message(
180                    *public_message,
181                    None,
182                    self.group_context()
183                        .tls_serialize_detached()
184                        .map_err(LibraryError::missing_bound_check)?,
185                    crypto,
186                    self.ciphersuite(),
187                )?
188            }
189        };
190
191        let unverified_message = self
192            .parse_message(decrypted_message, None)
193            .map_err(PublicProcessMessageError::from)?;
194        self.process_unverified_message(crypto, unverified_message)
195    }
196
197    /// Processes a public-group message and applies committed app-data update proposals.
198    ///
199    /// This is equivalent to [`Self::process_message`] for messages without app-data update
200    /// proposals. If app-data update proposals are committed by value, their resulting dictionary
201    /// changes are computed and supplied during staged commit validation.
202    #[cfg(feature = "extensions-draft-08")]
203    pub fn process_message_with_app_data_updates(
204        &self,
205        crypto: &impl OpenMlsCrypto,
206        message: impl Into<ProtocolMessage>,
207    ) -> Result<ProcessedMessage, PublicProcessMessageError> {
208        let protocol_message = message.into();
209        self.validate_framing(&protocol_message)?;
210
211        let decrypted_message = match protocol_message {
212            ProtocolMessage::PrivateMessage(_) => {
213                return Err(PublicProcessMessageError::IncompatibleWireFormat)
214            }
215            ProtocolMessage::PublicMessage(public_message) => {
216                DecryptedMessage::from_inbound_public_message(
217                    *public_message,
218                    None,
219                    self.group_context()
220                        .tls_serialize_detached()
221                        .map_err(LibraryError::missing_bound_check)?,
222                    crypto,
223                    self.ciphersuite(),
224                )?
225            }
226        };
227
228        let unverified_message = self
229            .parse_message(decrypted_message, None)
230            .map_err(PublicProcessMessageError::from)?;
231        let app_data_updates = self.extract_app_data_updates(&unverified_message);
232        self.process_unverified_message_with_app_data_updates(
233            crypto,
234            unverified_message,
235            app_data_updates,
236        )
237    }
238
239    #[cfg(feature = "extensions-draft-08")]
240    fn extract_app_data_updates(&self, unverified: &UnverifiedMessage) -> Option<AppDataUpdates> {
241        let mut updater = AppDataDictionaryUpdater::new(self.group_context().app_data_dict());
242        let mut updated = false;
243        for proposal in unverified.committed_proposals()? {
244            if let ProposalOrRefIn::Proposal(proposal) = proposal {
245                if let ProposalIn::AppDataUpdate(app_data_update) = &**proposal {
246                    match app_data_update.operation() {
247                        AppDataUpdateOperation::Update(data) => {
248                            updater.set(ComponentData::from_parts(
249                                app_data_update.component_id(),
250                                data.clone(),
251                            ));
252                        }
253                        AppDataUpdateOperation::Remove => {
254                            updater.remove(&app_data_update.component_id());
255                        }
256                    }
257                    updated = true;
258                }
259            }
260        }
261        updated.then(|| updater.changes()).flatten()
262    }
263}
264
265impl PublicGroup {
266    /// This processing function does most of the semantic verifications.
267    /// It returns a [ProcessedMessage] enum.
268    /// Checks the following semantic validation:
269    ///  - ValSem008
270    ///  - ValSem010
271    ///  - ValSem101
272    ///  - ValSem102
273    ///  - ValSem104
274    ///  - ValSem106
275    ///  - ValSem107
276    ///  - ValSem108
277    ///  - ValSem110
278    ///  - ValSem111
279    ///  - ValSem112
280    ///  - ValSem200
281    ///  - ValSem201
282    ///  - ValSem202: Path must be the right length
283    ///  - ValSem203: Path secrets must decrypt correctly
284    ///  - ValSem204: Public keys from Path must be verified and match the
285    ///    private keys from the direct path
286    ///  - ValSem205
287    ///  - ValSem240
288    ///  - ValSem241
289    ///  - ValSem242
290    ///  - ValSem244
291    ///  - ValSem246 (as part of ValSem010)
292    pub(crate) fn process_unverified_message(
293        &self,
294        crypto: &impl OpenMlsCrypto,
295        unverified_message: UnverifiedMessage,
296    ) -> Result<ProcessedMessage, PublicProcessMessageError> {
297        // Checks the following semantic validation:
298        //  - ValSem010
299        //  - ValSem246 (as part of ValSem010)
300        //  - https://validation.openmls.tech/#valn1203
301        let verified = unverified_message.verify(self.ciphersuite(), crypto, self.version())?;
302        let content = verified.content;
303        let credential = verified.credential;
304
305        #[cfg_attr(not(feature = "extensions-draft-08"), allow(unused_mut))]
306        let mut processed = match content.sender() {
307            Sender::Member(_) | Sender::NewMemberCommit | Sender::NewMemberProposal => {
308                self.process_internal_authenticated_content(crypto, content, credential)?
309            }
310            Sender::External(_) => {
311                self.process_external_authenticated_content(crypto, content, credential)?
312            }
313        };
314        #[cfg(feature = "extensions-draft-08")]
315        if self.group_context().safe_aad_required() {
316            processed
317                .try_attach_safe_aad()
318                .map_err(|_| PublicProcessMessageError::MalformedSafeAad)?;
319        }
320        Ok(processed)
321    }
322
323    /// This processing function does most of the semantic verifications.
324    /// It returns a [ProcessedMessage] enum.
325    /// Checks the following semantic validation:
326    ///  - ValSem008
327    ///  - ValSem010
328    ///  - ValSem101
329    ///  - ValSem102
330    ///  - ValSem104
331    ///  - ValSem106
332    ///  - ValSem107
333    ///  - ValSem108
334    ///  - ValSem110
335    ///  - ValSem111
336    ///  - ValSem112
337    ///  - ValSem200
338    ///  - ValSem201
339    ///  - ValSem202: Path must be the right length
340    ///  - ValSem203: Path secrets must decrypt correctly
341    ///  - ValSem204: Public keys from Path must be verified and match the
342    ///    private keys from the direct path
343    ///  - ValSem205
344    ///  - ValSem240
345    ///  - ValSem241
346    ///  - ValSem242
347    ///  - ValSem244
348    ///  - ValSem246 (as part of ValSem010)
349    #[cfg(feature = "extensions-draft-08")]
350    pub fn process_unverified_message_with_app_data_updates(
351        &self,
352        crypto: &impl OpenMlsCrypto,
353        unverified_message: UnverifiedMessage,
354        app_data_dict_updates: Option<AppDataUpdates>,
355    ) -> Result<ProcessedMessage, PublicProcessMessageError> {
356        // Checks the following semantic validation:
357        //  - ValSem010
358        //  - ValSem246 (as part of ValSem010)
359        //  - https://validation.openmls.tech/#valn1203
360        let verified = unverified_message.verify(self.ciphersuite(), crypto, self.version())?;
361        let content = verified.content;
362        let credential = verified.credential;
363
364        #[cfg_attr(not(feature = "extensions-draft-08"), allow(unused_mut))]
365        let mut processed = match content.sender() {
366            Sender::Member(_) | Sender::NewMemberCommit | Sender::NewMemberProposal => self
367                .process_internal_authenticated_content_with_app_data_updates(
368                    crypto,
369                    content,
370                    credential,
371                    app_data_dict_updates,
372                )?,
373            Sender::External(_) => {
374                self.process_external_authenticated_content(crypto, content, credential)?
375            }
376        };
377        #[cfg(feature = "extensions-draft-08")]
378        if self.group_context().safe_aad_required() {
379            processed
380                .try_attach_safe_aad()
381                .map_err(|_| PublicProcessMessageError::MalformedSafeAad)?;
382        }
383        Ok(processed)
384    }
385
386    fn process_internal_authenticated_content(
387        &self,
388        crypto: &impl OpenMlsCrypto,
389        content: AuthenticatedContent,
390        credential: Credential,
391    ) -> Result<ProcessedMessage, PublicProcessMessageError> {
392        let sender = content.sender().clone();
393        let authenticated_data = content.authenticated_data().to_owned();
394
395        let content = match content.content() {
396            FramedContentBody::Application(application_message) => {
397                ProcessedMessageContent::ApplicationMessage(ApplicationMessage::new(
398                    application_message.as_slice().to_owned(),
399                ))
400            }
401            FramedContentBody::Proposal(_) => {
402                let proposal = Box::new(QueuedProposal::from_authenticated_content_by_ref(
403                    self.ciphersuite(),
404                    crypto,
405                    content,
406                )?);
407                if matches!(sender, Sender::NewMemberProposal) {
408                    ProcessedMessageContent::ExternalJoinProposalMessage(proposal)
409                } else {
410                    ProcessedMessageContent::ProposalMessage(proposal)
411                }
412            }
413            FramedContentBody::Commit(_) => {
414                let staged_commit = self.stage_commit(&content, crypto)?;
415                ProcessedMessageContent::StagedCommitMessage(Box::new(staged_commit))
416            }
417        };
418
419        Ok(ProcessedMessage::new(
420            self.group_id().clone(),
421            self.group_context().epoch(),
422            sender,
423            authenticated_data,
424            content,
425            credential,
426            #[cfg(feature = "virtual-clients-draft")]
427            None,
428        ))
429    }
430
431    #[cfg(feature = "extensions-draft-08")]
432    fn process_internal_authenticated_content_with_app_data_updates(
433        &self,
434        crypto: &impl OpenMlsCrypto,
435        content: AuthenticatedContent,
436        credential: Credential,
437        app_data_dict_updates: Option<AppDataUpdates>,
438    ) -> Result<ProcessedMessage, PublicProcessMessageError> {
439        let sender = content.sender().clone();
440        let authenticated_data = content.authenticated_data().to_owned();
441
442        debug_assert!(matches!(
443            sender,
444            Sender::Member(_) | Sender::NewMemberCommit | Sender::NewMemberProposal
445        ));
446
447        let content = match content.content() {
448            FramedContentBody::Application(application_message) => {
449                ProcessedMessageContent::ApplicationMessage(ApplicationMessage::new(
450                    application_message.as_slice().to_owned(),
451                ))
452            }
453            FramedContentBody::Proposal(_) => {
454                let proposal = Box::new(QueuedProposal::from_authenticated_content_by_ref(
455                    self.ciphersuite(),
456                    crypto,
457                    content,
458                )?);
459                if matches!(sender, Sender::NewMemberProposal) {
460                    ProcessedMessageContent::ExternalJoinProposalMessage(proposal)
461                } else {
462                    ProcessedMessageContent::ProposalMessage(proposal)
463                }
464            }
465            FramedContentBody::Commit(_) => {
466                let staged_commit = self.stage_commit_with_app_data_updates(
467                    &content,
468                    crypto,
469                    app_data_dict_updates,
470                )?;
471
472                ProcessedMessageContent::StagedCommitMessage(Box::new(staged_commit))
473            }
474        };
475
476        Ok(ProcessedMessage::new(
477            self.group_id().clone(),
478            self.group_context().epoch(),
479            sender,
480            authenticated_data,
481            content,
482            credential,
483            #[cfg(feature = "virtual-clients-draft")]
484            None,
485        ))
486    }
487
488    fn process_external_authenticated_content(
489        &self,
490        crypto: &impl OpenMlsCrypto,
491        content: AuthenticatedContent,
492        credential: Credential,
493    ) -> Result<ProcessedMessage, PublicProcessMessageError> {
494        let sender = content.sender().clone();
495        let data = content.authenticated_data().to_owned();
496
497        debug_assert!(matches!(sender, Sender::External(_)));
498
499        // https://validation.openmls.tech/#valn1501
500        match content.content() {
501            FramedContentBody::Application(_) => {
502                Err(PublicProcessMessageError::UnauthorizedExternalApplicationMessage)
503            }
504            // TODO: https://validation.openmls.tech/#valn1502
505            FramedContentBody::Proposal(Proposal::GroupContextExtensions(_)) => {
506                let content = ProcessedMessageContent::ProposalMessage(Box::new(
507                    QueuedProposal::from_authenticated_content_by_ref(
508                        self.ciphersuite(),
509                        crypto,
510                        content,
511                    )?,
512                ));
513                Ok(ProcessedMessage::new(
514                    self.group_id().clone(),
515                    self.group_context().epoch(),
516                    sender,
517                    data,
518                    content,
519                    credential,
520                    #[cfg(feature = "virtual-clients-draft")]
521                    None,
522                ))
523            }
524
525            FramedContentBody::Proposal(Proposal::Remove(_)) => {
526                let content = ProcessedMessageContent::ProposalMessage(Box::new(
527                    QueuedProposal::from_authenticated_content_by_ref(
528                        self.ciphersuite(),
529                        crypto,
530                        content,
531                    )?,
532                ));
533                Ok(ProcessedMessage::new(
534                    self.group_id().clone(),
535                    self.group_context().epoch(),
536                    sender,
537                    data,
538                    content,
539                    credential,
540                    #[cfg(feature = "virtual-clients-draft")]
541                    None,
542                ))
543            }
544            FramedContentBody::Proposal(Proposal::Add(_)) => {
545                let content = ProcessedMessageContent::ProposalMessage(Box::new(
546                    QueuedProposal::from_authenticated_content_by_ref(
547                        self.ciphersuite(),
548                        crypto,
549                        content,
550                    )?,
551                ));
552                Ok(ProcessedMessage::new(
553                    self.group_id().clone(),
554                    self.group_context().epoch(),
555                    sender,
556                    data,
557                    content,
558                    credential,
559                    #[cfg(feature = "virtual-clients-draft")]
560                    None,
561                ))
562            }
563            // TODO #151/#106
564            FramedContentBody::Proposal(_) => {
565                Err(PublicProcessMessageError::UnsupportedProposalType)
566            }
567            FramedContentBody::Commit(_) => {
568                Err(PublicProcessMessageError::UnauthorizedExternalCommitMessage)
569            }
570        }
571    }
572}