Skip to main content

openmls/framing/
validation.rs

1//! # Validation steps for incoming messages
2//!
3//! ```text
4//!
5//!                             MlsMessageIn
6//!                                  │                    -.
7//!                                  │                      │
8//!                                  │                      │
9//!                                  ▼                      │
10//!                           DecryptedMessage              +-- parse_message
11//!                                  │                      │
12//!                                  │                      │
13//!                                  │                      │
14//!                                  ▼                    -'
15//!                           UnverifiedMessage
16//!                                  │                    -.
17//!                                  │                      │
18//!                                  │                      +-- process_unverified_message
19//!                                  │                      │
20//!                                  ▼                    -'
21//!                          ProcessedMessage
22//!
23//! ```
24
25use openmls_traits::{crypto::OpenMlsCrypto, types::Ciphersuite};
26use proposal_store::QueuedProposal;
27
28use crate::{
29    binary_tree::LeafNodeIndex,
30    ciphersuite::signable::Verifiable,
31    error::LibraryError,
32    extensions::ExternalSendersExtension,
33    group::{errors::ValidationError, mls_group::staged_commit::StagedCommit},
34    tree::sender_ratchet::SenderRatchetConfiguration,
35    versions::ProtocolVersion,
36};
37
38#[cfg(feature = "extensions-draft")]
39use crate::{
40    component::ComponentId, framing::safe_aad::SafeAad, messages::proposals_in::ProposalOrRefIn,
41};
42
43use super::{
44    mls_auth_content::AuthenticatedContent,
45    mls_auth_content_in::{AuthenticatedContentIn, VerifiableAuthenticatedContentIn},
46    private_message_in::PrivateMessageIn,
47    public_message_in::PublicMessageIn,
48    *,
49};
50
51/// Intermediate message that can be constructed either from a public message or from private message.
52/// If it it constructed from a ciphertext message, the ciphertext message is decrypted first.
53/// This function implements the following checks:
54///  - ValSem005
55///  - ValSem007
56///  - ValSem009
57#[derive(Debug)]
58pub(crate) struct DecryptedMessage {
59    verifiable_content: VerifiableAuthenticatedContentIn,
60    /// Recovered sender emulation-group leaf index for an application
61    /// message from a sibling emulator client.
62    #[cfg(feature = "virtual-clients-draft")]
63    emulator_sender_leaf_index: Option<LeafNodeIndex>,
64}
65
66impl DecryptedMessage {
67    /// Constructs a [DecryptedMessage] from a [VerifiableAuthenticatedContent].
68    pub(crate) fn from_inbound_public_message<'a>(
69        public_message: PublicMessageIn,
70        message_secrets_option: impl Into<Option<&'a MessageSecrets>>,
71        serialized_context: Vec<u8>,
72        crypto: &impl OpenMlsCrypto,
73        ciphersuite: Ciphersuite,
74    ) -> Result<Self, ValidationError> {
75        if public_message.sender().is_member() {
76            // ValSem007 Membership tag presence
77            if public_message.membership_tag().is_none() {
78                return Err(ValidationError::MissingMembershipTag);
79            }
80
81            if let Some(message_secrets) = message_secrets_option.into() {
82                // Verify the membership tag. This needs to be done explicitly for PublicMessage messages,
83                // it is implicit for PrivateMessage messages (because the encryption can only be known by members).
84                // ValSem008
85                // https://validation.openmls.tech/#valn1302
86                public_message.verify_membership(
87                    crypto,
88                    ciphersuite,
89                    message_secrets.membership_key(),
90                    message_secrets.serialized_context(),
91                )?;
92            }
93        }
94
95        let verifiable_content = public_message.into_verifiable_content(serialized_context);
96
97        // Public messages don't carry a reuse_guard, so no emulator
98        // sender leaf index to recover.
99        Self::from_verifiable_content(
100            verifiable_content,
101            #[cfg(feature = "virtual-clients-draft")]
102            None,
103        )
104    }
105
106    /// Constructs a [DecryptedMessage] from a [PrivateMessage] by attempting to decrypt it
107    /// to a [VerifiableAuthenticatedContent] first.
108    pub(crate) fn from_inbound_ciphertext(
109        ciphertext: PrivateMessageIn,
110        crypto: &impl OpenMlsCrypto,
111        group: &mut MlsGroup,
112        sender_ratchet_configuration: &SenderRatchetConfiguration,
113        #[cfg(feature = "virtual-clients-draft")] emulator_ctx: Option<
114            &crate::framing::private_message::EmulatorReuseGuardCtx<'_>,
115        >,
116    ) -> Result<Self, ValidationError> {
117        // This will be refactored with #265.
118        let ciphersuite = group.ciphersuite();
119        // TODO: #819 The old leaves should not be needed any more.
120        //       Revisit when the transition is further along.
121        let (message_secrets, _old_leaves) = group
122            .message_secrets_and_leaves(ciphertext.epoch())
123            .map_err(MessageDecryptionError::SecretTreeError)?;
124        let sender_data = ciphertext.sender_data(message_secrets, crypto, ciphersuite)?;
125        // Check if we are the sender. With the `virtual-clients` feature,
126        // decrypting own messages is allowed, so we skip this check
127        #[cfg(not(feature = "virtual-clients-draft"))]
128        if sender_data.leaf_index == group.own_leaf_index() {
129            return Err(ValidationError::CannotDecryptOwnMessage);
130        }
131        #[cfg(feature = "virtual-clients-draft")]
132        let effective_emulator_ctx = match emulator_ctx {
133            Some(ctx) if sender_data.leaf_index == group.own_leaf_index() => Some(ctx),
134            _ => None,
135        };
136        let message_secrets = group
137            .message_secrets_for_epoch_mut(ciphertext.epoch())
138            .map_err(|_| MessageDecryptionError::AeadError)?;
139        let decrypted = ciphertext.to_verifiable_content(
140            ciphersuite,
141            crypto,
142            message_secrets,
143            sender_data.leaf_index,
144            sender_ratchet_configuration,
145            sender_data,
146            #[cfg(feature = "virtual-clients-draft")]
147            effective_emulator_ctx,
148        )?;
149        Self::from_verifiable_content(
150            decrypted.verifiable,
151            #[cfg(feature = "virtual-clients-draft")]
152            decrypted.emulator_sender_leaf_index,
153        )
154    }
155
156    // Internal constructor function. Does the following checks:
157    // - Confirmation tag must be present for Commit messages
158    // - Membership tag must be present for member messages, if the original incoming message was not an PrivateMessage
159    // - Ensures application messages were originally PrivateMessage messages
160    fn from_verifiable_content(
161        verifiable_content: VerifiableAuthenticatedContentIn,
162        #[cfg(feature = "virtual-clients-draft")] emulator_sender_leaf_index: Option<LeafNodeIndex>,
163    ) -> Result<Self, ValidationError> {
164        // ValSem009
165        if verifiable_content.content_type() == ContentType::Commit
166            && verifiable_content.confirmation_tag().is_none()
167        {
168            return Err(ValidationError::MissingConfirmationTag);
169        }
170        // ValSem005
171        if verifiable_content.content_type() == ContentType::Application {
172            if verifiable_content.wire_format() != WireFormat::PrivateMessage {
173                return Err(ValidationError::UnencryptedApplicationMessage);
174            } else if !verifiable_content.sender().is_member() {
175                // This should not happen because the sender of an PrivateMessage should always be a member
176                return Err(LibraryError::custom("Expected sender to be member.").into());
177            }
178        }
179        Ok(DecryptedMessage {
180            verifiable_content,
181            #[cfg(feature = "virtual-clients-draft")]
182            emulator_sender_leaf_index,
183        })
184    }
185
186    /// Recovered sender emulation-group leaf index, if the message came
187    /// from a sibling emulator client.
188    #[cfg(feature = "virtual-clients-draft")]
189    #[allow(dead_code)]
190    pub(crate) fn emulator_sender_leaf_index(&self) -> Option<LeafNodeIndex> {
191        self.emulator_sender_leaf_index
192    }
193
194    /// Gets the correct credential from the message depending on the sender type.
195    ///
196    /// The closure argument is used to look up the credential and signature key. If the epoch of
197    /// the message is the same as that of the group, look it up in the tree; else, look in up in
198    /// the past trees of the message secret store.
199    ///
200    /// Checks the following semantic validation:
201    ///  - ValSem112
202    ///  - ValSem245
203    ///  - Prepares ValSem246 by setting the right credential. The remainder
204    ///    of ValSem246 is validated as part of ValSem010.
205    ///  - [valn1301](https://validation.openmls.tech/#valn1301)
206    ///
207    /// Returns the [`Credential`] and the leaf's [`SignaturePublicKey`].
208    pub(crate) fn credential(
209        &self,
210        look_up_credential_with_key: impl Fn(LeafNodeIndex) -> Option<CredentialWithKey>,
211        external_senders: Option<&ExternalSendersExtension>,
212    ) -> Result<CredentialWithKey, ValidationError> {
213        let sender = self.sender();
214        match sender {
215            Sender::Member(leaf_index) => {
216                // https://validation.openmls.tech/#valn1306
217                look_up_credential_with_key(*leaf_index).ok_or(ValidationError::UnknownMember)
218            }
219            Sender::External(index) => {
220                let sender = external_senders
221                    .ok_or(ValidationError::NoExternalSendersExtension)?
222                    .get(index.index())
223                    .ok_or(ValidationError::UnauthorizedExternalSender)?;
224                Ok(CredentialWithKey {
225                    credential: sender.credential().clone(),
226                    signature_key: sender.signature_key().clone(),
227                })
228            }
229            Sender::NewMemberCommit | Sender::NewMemberProposal => {
230                // Fetch the credential from the message itself.
231                // https://validation.openmls.tech/#valn0407
232                self.verifiable_content.new_member_credential()
233            }
234        }
235    }
236
237    /// Returns the sender.
238    pub fn sender(&self) -> &Sender {
239        self.verifiable_content.sender()
240    }
241
242    /// Returns the [`VerifiableAuthenticatedContent`].
243    pub(crate) fn verifiable_content(&self) -> &VerifiableAuthenticatedContentIn {
244        &self.verifiable_content
245    }
246}
247
248/// Result of [`UnverifiedMessage::verify`].
249pub(crate) struct VerifiedMessage {
250    pub(crate) content: AuthenticatedContent,
251    pub(crate) credential: Credential,
252    #[cfg(feature = "virtual-clients-draft")]
253    pub(crate) emulator_sender_leaf_index: Option<LeafNodeIndex>,
254}
255
256/// Context that is needed to verify the signature of a the leaf node of an
257/// UpdatePath or an update proposal.
258#[derive(Debug, Clone)]
259pub(crate) enum SenderContext {
260    Member((GroupId, LeafNodeIndex)),
261    ExternalCommit {
262        group_id: GroupId,
263        leftmost_blank_index: LeafNodeIndex,
264        self_removes_in_store: Vec<SelfRemoveInStore>,
265    },
266}
267
268/// Partially checked and potentially decrypted message (if it was originally encrypted).
269/// Use this to inspect the [`Credential`] of the message sender
270/// and the optional `aad` if the original message was encrypted.
271/// The [`OpenMlsSignaturePublicKey`] is used to verify the signature of the
272/// message.
273#[derive(Debug, Clone)]
274pub struct UnverifiedMessage {
275    verifiable_content: VerifiableAuthenticatedContentIn,
276    credential: Credential,
277    sender_pk: OpenMlsSignaturePublicKey,
278    sender_context: Option<SenderContext>,
279    /// See [`DecryptedMessage::emulator_sender_leaf_index`].
280    #[cfg(feature = "virtual-clients-draft")]
281    emulator_sender_leaf_index: Option<LeafNodeIndex>,
282}
283
284impl UnverifiedMessage {
285    /// Construct an [UnverifiedMessage] from a [DecryptedMessage] and an optional [Credential].
286    pub(crate) fn from_decrypted_message(
287        decrypted_message: DecryptedMessage,
288        credential: Credential,
289        sender_pk: OpenMlsSignaturePublicKey,
290        sender_context: Option<SenderContext>,
291    ) -> Self {
292        #[cfg(feature = "virtual-clients-draft")]
293        let emulator_sender_leaf_index = decrypted_message.emulator_sender_leaf_index;
294        UnverifiedMessage {
295            verifiable_content: decrypted_message.verifiable_content,
296            credential,
297            sender_pk,
298            sender_context,
299            #[cfg(feature = "virtual-clients-draft")]
300            emulator_sender_leaf_index,
301        }
302    }
303
304    /// Verify the [`UnverifiedMessage`].
305    pub(crate) fn verify(
306        self,
307        ciphersuite: Ciphersuite,
308        crypto: &impl OpenMlsCrypto,
309        protocol_version: ProtocolVersion,
310    ) -> Result<VerifiedMessage, ValidationError> {
311        let content: AuthenticatedContentIn = self
312            .verifiable_content
313            .verify(crypto, &self.sender_pk)
314            .map_err(|_| ValidationError::InvalidSignature)?;
315        // https://validation.openmls.tech/#valn1302
316        // https://validation.openmls.tech/#valn1304
317        let content =
318            content.validate(ciphersuite, crypto, self.sender_context, protocol_version)?;
319        Ok(VerifiedMessage {
320            content,
321            credential: self.credential,
322            #[cfg(feature = "virtual-clients-draft")]
323            emulator_sender_leaf_index: self.emulator_sender_leaf_index,
324        })
325    }
326
327    /// Get the proposals of the commit, if it is one. If not, return `None`.
328    #[cfg(feature = "extensions-draft")]
329    pub fn committed_proposals(&self) -> Option<&[ProposalOrRefIn]> {
330        self.verifiable_content.committed_proposals()
331    }
332}
333
334/// A message that has passed all syntax and semantics checks.
335#[derive(Debug)]
336pub struct ProcessedMessage {
337    group_id: GroupId,
338    epoch: GroupEpoch,
339    sender: Sender,
340    authenticated_data: Vec<u8>,
341    content: ProcessedMessageContent,
342    credential: Credential,
343    /// See [`Self::emulator_sender_leaf_index`].
344    #[cfg(feature = "virtual-clients-draft")]
345    emulator_sender_leaf_index: Option<LeafNodeIndex>,
346    /// Parsed Safe AAD prefix, populated only when the message's GroupContext
347    /// required Safe AAD framing. `None` otherwise.
348    #[cfg(feature = "extensions-draft")]
349    safe_aad: Option<SafeAad>,
350    /// Length in bytes of the Safe AAD prefix at the start of
351    /// `authenticated_data`. Zero when [`Self::safe_aad`] is `None`.
352    #[cfg(feature = "extensions-draft")]
353    safe_aad_prefix_len: usize,
354}
355
356impl ProcessedMessage {
357    /// Create a new `ProcessedMessage`.
358    pub(crate) fn new(
359        group_id: GroupId,
360        epoch: GroupEpoch,
361        sender: Sender,
362        authenticated_data: Vec<u8>,
363        content: ProcessedMessageContent,
364        credential: Credential,
365        #[cfg(feature = "virtual-clients-draft")] emulator_sender_leaf_index: Option<LeafNodeIndex>,
366    ) -> Self {
367        Self {
368            group_id,
369            epoch,
370            sender,
371            authenticated_data,
372            content,
373            credential,
374            #[cfg(feature = "virtual-clients-draft")]
375            emulator_sender_leaf_index,
376            #[cfg(feature = "extensions-draft")]
377            safe_aad: None,
378            #[cfg(feature = "extensions-draft")]
379            safe_aad_prefix_len: 0,
380        }
381    }
382
383    /// Parse the Safe AAD prefix at the start of `authenticated_data` and
384    /// attach it to this message. Callers should invoke this only when the receiving
385    /// group's GroupContext requires Safe AAD framing. Otherwise, `safe_aad`
386    /// stays `None` and `authenticated_data` is the caller-supplied bytes
387    /// untouched.
388    #[cfg(feature = "extensions-draft")]
389    pub(crate) fn try_attach_safe_aad(&mut self) -> Result<(), crate::framing::SafeAadError> {
390        let (safe_aad, prefix_len) =
391            crate::framing::safe_aad::parse_authenticated_data_prefix(&self.authenticated_data)?;
392        self.safe_aad = Some(safe_aad);
393        self.safe_aad_prefix_len = prefix_len;
394        Ok(())
395    }
396
397    /// Returns the parsed Safe AAD struct, or `None` if Safe AAD was not
398    /// active for the group this message belongs to.
399    #[cfg(feature = "extensions-draft")]
400    pub fn safe_aad(&self) -> Option<&SafeAad> {
401        self.safe_aad.as_ref()
402    }
403
404    /// Look up a Safe AAD item by [`ComponentId`].
405    #[cfg(feature = "extensions-draft")]
406    pub fn safe_aad_item(&self, component_id: crate::component::ComponentId) -> Option<&[u8]> {
407        self.safe_aad
408            .as_ref()
409            .and_then(|safe_aad| safe_aad.get(component_id))
410    }
411
412    /// Returns the bytes of `authenticated_data` after any Safe AAD prefix.
413    /// Equal to [`Self::aad`] when no Safe AAD prefix is present.
414    #[cfg(feature = "extensions-draft")]
415    pub fn tail_aad(&self) -> &[u8] {
416        &self.authenticated_data[self.safe_aad_prefix_len..]
417    }
418
419    /// Returns the sender's leaf index in the emulation group when this
420    /// message is an application message from a sibling emulator client.
421    #[cfg(feature = "virtual-clients-draft")]
422    pub fn emulator_sender_leaf_index(&self) -> Option<LeafNodeIndex> {
423        self.emulator_sender_leaf_index
424    }
425
426    /// Returns the group ID of the message.
427    pub fn group_id(&self) -> &GroupId {
428        &self.group_id
429    }
430
431    /// Returns the epoch of the message.
432    pub fn epoch(&self) -> GroupEpoch {
433        self.epoch
434    }
435
436    /// Returns the sender of the message.
437    pub fn sender(&self) -> &Sender {
438        &self.sender
439    }
440
441    /// Returns the additional authenticated data (AAD) of the message.
442    pub fn aad(&self) -> &[u8] {
443        &self.authenticated_data
444    }
445
446    /// Returns the content of the message.
447    pub fn content(&self) -> &ProcessedMessageContent {
448        &self.content
449    }
450
451    /// Returns the content of the message and consumes the message.
452    pub fn into_content(self) -> ProcessedMessageContent {
453        self.content
454    }
455
456    /// Returns the credential of the message.
457    pub fn credential(&self) -> &Credential {
458        &self.credential
459    }
460
461    /// Safely export a value if the content of the processed message is a
462    /// [`StagedCommit`].
463    #[cfg(feature = "extensions-draft")]
464    pub fn safe_export_secret<Crypto: OpenMlsCrypto>(
465        &mut self,
466        crypto: &Crypto,
467        component_id: ComponentId,
468    ) -> Result<Vec<u8>, ProcessedMessageSafeExportSecretError> {
469        if let ProcessedMessageContent::StagedCommitMessage(ref mut staged_commit) =
470            &mut self.content
471        {
472            let secret = staged_commit.safe_export_secret(crypto, component_id)?;
473            Ok(secret)
474        } else {
475            Err(ProcessedMessageSafeExportSecretError::NotACommit)
476        }
477    }
478}
479
480/// Content of a processed message.
481///
482/// See the content variants' documentation for more information.
483/// [`StagedCommit`] and [`QueuedProposal`] can be inspected for authorization purposes.
484#[derive(Debug)]
485pub enum ProcessedMessageContent {
486    /// An application message.
487    ///
488    /// The [`ApplicationMessage`] contains a vector of bytes that can be used right-away.
489    ApplicationMessage(ApplicationMessage),
490    /// A standalone proposal.
491    ///
492    /// The [`QueuedProposal`] can be inspected for authorization purposes by the application.
493    /// If the proposal is deemed to be allowed, it should be added to the group's proposal
494    /// queue using [`MlsGroup::store_pending_proposal()`](crate::group::mls_group::MlsGroup::store_pending_proposal()).
495    ProposalMessage(Box<QueuedProposal>),
496    /// An [external join proposal](crate::prelude::JoinProposal) sent by a
497    /// [NewMemberProposal](crate::prelude::Sender::NewMemberProposal) sender which is outside the group.
498    ///
499    /// Since this originates from a party outside the group, the [`QueuedProposal`] SHOULD be
500    /// inspected for authorization purposes by the application. If the proposal is deemed to be
501    /// allowed, it should be added to the group's proposal queue using
502    /// [`MlsGroup::store_pending_proposal()`](crate::group::mls_group::MlsGroup::store_pending_proposal()).
503    ExternalJoinProposalMessage(Box<QueuedProposal>),
504    /// A Commit message.
505    ///
506    /// The [`StagedCommit`] can be inspected for authorization purposes by the application.
507    /// If the type of the commit and the proposals it covers are deemed to be allowed,
508    /// the commit should be merged into the group's state using
509    /// [`MlsGroup::merge_staged_commit()`](crate::group::mls_group::MlsGroup::merge_staged_commit()).
510    StagedCommitMessage(Box<StagedCommit>),
511    /// A Commit authored by this client that it got fanned out by the delivery
512    /// service, matching the group's pending commit.
513    ///
514    /// This is returned instead of
515    /// [`StagedCommitMessage`](Self::StagedCommitMessage) when the processed
516    /// Commit was created by this client and matches the group's pending commit.
517    /// Since this client already holds the corresponding pending commit, the
518    /// incoming Commit is not staged. To apply it, merge the pending commit
519    /// using
520    /// [`MlsGroup::merge_pending_commit()`](crate::group::mls_group::MlsGroup::merge_pending_commit()).
521    /// An own Commit that does not match the pending commit is instead returned
522    /// as a [`StagedCommitMessage`](Self::StagedCommitMessage) (if it has no
523    /// UpdatePath) or rejected (if it has an UpdatePath we cannot decrypt).
524    ///
525    /// The match against the pending commit is established by comparing the
526    /// confirmation tag of the incoming Commit against the one stored with the
527    /// pending commit. The message signature has already been verified, which
528    /// authenticates the Commit as ours, and a matching confirmation tag binds
529    /// the confirmed transcript hash of the new epoch. We do not otherwise
530    /// compare the contents of the incoming Commit against the pending commit,
531    /// and the incoming Commit's state is never adopted.
532    ///
533    /// This is only produced for Commits framed as
534    /// [`PublicMessage`](crate::framing::MlsMessageBodyIn::PublicMessage). A
535    /// Commit framed as a
536    /// [`PrivateMessage`](crate::framing::MlsMessageBodyIn::PrivateMessage)
537    /// cannot be decrypted by its own author and is instead rejected during
538    /// decryption.
539    OwnPendingCommit,
540}
541
542/// Application message received through a [ProcessedMessage].
543#[derive(Debug, PartialEq, Eq)]
544pub struct ApplicationMessage {
545    bytes: Vec<u8>,
546}
547
548impl ApplicationMessage {
549    /// Create a new [ApplicationMessage].
550    pub(crate) fn new(bytes: Vec<u8>) -> Self {
551        Self { bytes }
552    }
553
554    /// Returns the inner bytes and consumes the [`ApplicationMessage`].
555    pub fn into_bytes(self) -> Vec<u8> {
556        self.bytes
557    }
558}