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-08")]
39use crate::messages::proposals_in::ProposalOrRefIn;
40
41use super::{
42    mls_auth_content::AuthenticatedContent,
43    mls_auth_content_in::{AuthenticatedContentIn, VerifiableAuthenticatedContentIn},
44    private_message_in::PrivateMessageIn,
45    public_message_in::PublicMessageIn,
46    *,
47};
48
49/// Intermediate message that can be constructed either from a public message or from private message.
50/// If it it constructed from a ciphertext message, the ciphertext message is decrypted first.
51/// This function implements the following checks:
52///  - ValSem005
53///  - ValSem007
54///  - ValSem009
55#[derive(Debug)]
56pub(crate) struct DecryptedMessage {
57    verifiable_content: VerifiableAuthenticatedContentIn,
58}
59
60impl DecryptedMessage {
61    /// Constructs a [DecryptedMessage] from a [VerifiableAuthenticatedContent].
62    pub(crate) fn from_inbound_public_message<'a>(
63        public_message: PublicMessageIn,
64        message_secrets_option: impl Into<Option<&'a MessageSecrets>>,
65        serialized_context: Vec<u8>,
66        crypto: &impl OpenMlsCrypto,
67        ciphersuite: Ciphersuite,
68    ) -> Result<Self, ValidationError> {
69        if public_message.sender().is_member() {
70            // ValSem007 Membership tag presence
71            if public_message.membership_tag().is_none() {
72                return Err(ValidationError::MissingMembershipTag);
73            }
74
75            if let Some(message_secrets) = message_secrets_option.into() {
76                // Verify the membership tag. This needs to be done explicitly for PublicMessage messages,
77                // it is implicit for PrivateMessage messages (because the encryption can only be known by members).
78                // ValSem008
79                // https://validation.openmls.tech/#valn1302
80                public_message.verify_membership(
81                    crypto,
82                    ciphersuite,
83                    message_secrets.membership_key(),
84                    message_secrets.serialized_context(),
85                )?;
86            }
87        }
88
89        let verifiable_content = public_message.into_verifiable_content(serialized_context);
90
91        Self::from_verifiable_content(verifiable_content)
92    }
93
94    /// Constructs a [DecryptedMessage] from a [PrivateMessage] by attempting to decrypt it
95    /// to a [VerifiableAuthenticatedContent] first.
96    pub(crate) fn from_inbound_ciphertext(
97        ciphertext: PrivateMessageIn,
98        crypto: &impl OpenMlsCrypto,
99        group: &mut MlsGroup,
100        sender_ratchet_configuration: &SenderRatchetConfiguration,
101    ) -> Result<Self, ValidationError> {
102        // This will be refactored with #265.
103        let ciphersuite = group.ciphersuite();
104        // TODO: #819 The old leaves should not be needed any more.
105        //       Revisit when the transition is further along.
106        let (message_secrets, _old_leaves) = group
107            .message_secrets_and_leaves_mut(ciphertext.epoch())
108            .map_err(MessageDecryptionError::SecretTreeError)?;
109        let sender_data = ciphertext.sender_data(message_secrets, crypto, ciphersuite)?;
110        // Check if we are the sender
111        if sender_data.leaf_index == group.own_leaf_index() {
112            return Err(ValidationError::CannotDecryptOwnMessage);
113        }
114        let message_secrets = group
115            .message_secrets_mut(ciphertext.epoch())
116            .map_err(|_| MessageDecryptionError::AeadError)?;
117        let verifiable_content = ciphertext.to_verifiable_content(
118            ciphersuite,
119            crypto,
120            message_secrets,
121            sender_data.leaf_index,
122            sender_ratchet_configuration,
123            sender_data,
124        )?;
125        Self::from_verifiable_content(verifiable_content)
126    }
127
128    // Internal constructor function. Does the following checks:
129    // - Confirmation tag must be present for Commit messages
130    // - Membership tag must be present for member messages, if the original incoming message was not an PrivateMessage
131    // - Ensures application messages were originally PrivateMessage messages
132    fn from_verifiable_content(
133        verifiable_content: VerifiableAuthenticatedContentIn,
134    ) -> Result<Self, ValidationError> {
135        // ValSem009
136        if verifiable_content.content_type() == ContentType::Commit
137            && verifiable_content.confirmation_tag().is_none()
138        {
139            return Err(ValidationError::MissingConfirmationTag);
140        }
141        // ValSem005
142        if verifiable_content.content_type() == ContentType::Application {
143            if verifiable_content.wire_format() != WireFormat::PrivateMessage {
144                return Err(ValidationError::UnencryptedApplicationMessage);
145            } else if !verifiable_content.sender().is_member() {
146                // This should not happen because the sender of an PrivateMessage should always be a member
147                return Err(LibraryError::custom("Expected sender to be member.").into());
148            }
149        }
150        Ok(DecryptedMessage { verifiable_content })
151    }
152
153    /// Gets the correct credential from the message depending on the sender type.
154    ///
155    /// The closure argument is used to look up the credential and signature key. If the epoch of
156    /// the message is the same as that of the group, look it up in the tree; else, look in up in
157    /// the past trees of the message secret store.
158    ///
159    /// Checks the following semantic validation:
160    ///  - ValSem112
161    ///  - ValSem245
162    ///  - Prepares ValSem246 by setting the right credential. The remainder
163    ///    of ValSem246 is validated as part of ValSem010.
164    ///  - [valn1301](https://validation.openmls.tech/#valn1301)
165    ///
166    /// Returns the [`Credential`] and the leaf's [`SignaturePublicKey`].
167    pub(crate) fn credential(
168        &self,
169        look_up_credential_with_key: impl Fn(LeafNodeIndex) -> Option<CredentialWithKey>,
170        external_senders: Option<&ExternalSendersExtension>,
171    ) -> Result<CredentialWithKey, ValidationError> {
172        let sender = self.sender();
173        match sender {
174            Sender::Member(leaf_index) => {
175                // https://validation.openmls.tech/#valn1306
176                look_up_credential_with_key(*leaf_index).ok_or(ValidationError::UnknownMember)
177            }
178            Sender::External(index) => {
179                let sender = external_senders
180                    .ok_or(ValidationError::NoExternalSendersExtension)?
181                    .get(index.index())
182                    .ok_or(ValidationError::UnauthorizedExternalSender)?;
183                Ok(CredentialWithKey {
184                    credential: sender.credential().clone(),
185                    signature_key: sender.signature_key().clone(),
186                })
187            }
188            Sender::NewMemberCommit | Sender::NewMemberProposal => {
189                // Fetch the credential from the message itself.
190                // https://validation.openmls.tech/#valn0407
191                self.verifiable_content.new_member_credential()
192            }
193        }
194    }
195
196    /// Returns the sender.
197    pub fn sender(&self) -> &Sender {
198        self.verifiable_content.sender()
199    }
200
201    /// Returns the [`VerifiableAuthenticatedContent`].
202    pub(crate) fn verifiable_content(&self) -> &VerifiableAuthenticatedContentIn {
203        &self.verifiable_content
204    }
205}
206
207/// Context that is needed to verify the signature of a the leaf node of an
208/// UpdatePath or an update proposal.
209#[derive(Debug, Clone)]
210pub(crate) enum SenderContext {
211    Member((GroupId, LeafNodeIndex)),
212    ExternalCommit {
213        group_id: GroupId,
214        leftmost_blank_index: LeafNodeIndex,
215        self_removes_in_store: Vec<SelfRemoveInStore>,
216    },
217}
218
219/// Partially checked and potentially decrypted message (if it was originally encrypted).
220/// Use this to inspect the [`Credential`] of the message sender
221/// and the optional `aad` if the original message was encrypted.
222/// The [`OpenMlsSignaturePublicKey`] is used to verify the signature of the
223/// message.
224#[derive(Debug, Clone)]
225pub struct UnverifiedMessage {
226    verifiable_content: VerifiableAuthenticatedContentIn,
227    credential: Credential,
228    sender_pk: OpenMlsSignaturePublicKey,
229    sender_context: Option<SenderContext>,
230}
231
232impl UnverifiedMessage {
233    /// Construct an [UnverifiedMessage] from a [DecryptedMessage] and an optional [Credential].
234    pub(crate) fn from_decrypted_message(
235        decrypted_message: DecryptedMessage,
236        credential: Credential,
237        sender_pk: OpenMlsSignaturePublicKey,
238        sender_context: Option<SenderContext>,
239    ) -> Self {
240        UnverifiedMessage {
241            verifiable_content: decrypted_message.verifiable_content,
242            credential,
243            sender_pk,
244            sender_context,
245        }
246    }
247
248    /// Verify the [`UnverifiedMessage`]. Returns the [`AuthenticatedContent`]
249    /// and the internal [`Credential`].
250    pub(crate) fn verify(
251        self,
252        ciphersuite: Ciphersuite,
253        crypto: &impl OpenMlsCrypto,
254        protocol_version: ProtocolVersion,
255    ) -> Result<(AuthenticatedContent, Credential), ValidationError> {
256        let content: AuthenticatedContentIn = self
257            .verifiable_content
258            .verify(crypto, &self.sender_pk)
259            .map_err(|_| ValidationError::InvalidSignature)?;
260        // https://validation.openmls.tech/#valn1302
261        // https://validation.openmls.tech/#valn1304
262        let content =
263            content.validate(ciphersuite, crypto, self.sender_context, protocol_version)?;
264        Ok((content, self.credential))
265    }
266
267    /// Get the proposals of the commit, if it is one. If not, return `None`.
268    #[cfg(feature = "extensions-draft-08")]
269    pub fn committed_proposals(&self) -> Option<&[ProposalOrRefIn]> {
270        self.verifiable_content.committed_proposals()
271    }
272}
273
274/// A message that has passed all syntax and semantics checks.
275#[derive(Debug)]
276pub struct ProcessedMessage {
277    group_id: GroupId,
278    epoch: GroupEpoch,
279    sender: Sender,
280    authenticated_data: Vec<u8>,
281    content: ProcessedMessageContent,
282    credential: Credential,
283}
284
285impl ProcessedMessage {
286    /// Create a new `ProcessedMessage`.
287    pub(crate) fn new(
288        group_id: GroupId,
289        epoch: GroupEpoch,
290        sender: Sender,
291        authenticated_data: Vec<u8>,
292        content: ProcessedMessageContent,
293        credential: Credential,
294    ) -> Self {
295        Self {
296            group_id,
297            epoch,
298            sender,
299            authenticated_data,
300            content,
301            credential,
302        }
303    }
304
305    /// Returns the group ID of the message.
306    pub fn group_id(&self) -> &GroupId {
307        &self.group_id
308    }
309
310    /// Returns the epoch of the message.
311    pub fn epoch(&self) -> GroupEpoch {
312        self.epoch
313    }
314
315    /// Returns the sender of the message.
316    pub fn sender(&self) -> &Sender {
317        &self.sender
318    }
319
320    /// Returns the additional authenticated data (AAD) of the message.
321    pub fn aad(&self) -> &[u8] {
322        &self.authenticated_data
323    }
324
325    /// Returns the content of the message.
326    pub fn content(&self) -> &ProcessedMessageContent {
327        &self.content
328    }
329
330    /// Returns the content of the message and consumes the message.
331    pub fn into_content(self) -> ProcessedMessageContent {
332        self.content
333    }
334
335    /// Returns the credential of the message.
336    pub fn credential(&self) -> &Credential {
337        &self.credential
338    }
339
340    /// Safely export a value if the content of the processed message is a
341    /// [`StagedCommit`].
342    #[cfg(feature = "extensions-draft-08")]
343    pub fn safe_export_secret<Crypto: OpenMlsCrypto>(
344        &mut self,
345        crypto: &Crypto,
346        component_id: u16,
347    ) -> Result<Vec<u8>, ProcessedMessageSafeExportSecretError> {
348        if let ProcessedMessageContent::StagedCommitMessage(ref mut staged_commit) =
349            &mut self.content
350        {
351            let secret = staged_commit.safe_export_secret(crypto, component_id)?;
352            Ok(secret)
353        } else {
354            Err(ProcessedMessageSafeExportSecretError::NotACommit)
355        }
356    }
357}
358
359/// Content of a processed message.
360///
361/// See the content variants' documentation for more information.
362/// [`StagedCommit`] and [`QueuedProposal`] can be inspected for authorization purposes.
363#[derive(Debug)]
364pub enum ProcessedMessageContent {
365    /// An application message.
366    ///
367    /// The [`ApplicationMessage`] contains a vector of bytes that can be used right-away.
368    ApplicationMessage(ApplicationMessage),
369    /// A standalone proposal.
370    ///
371    /// The [`QueuedProposal`] can be inspected for authorization purposes by the application.
372    /// If the proposal is deemed to be allowed, it should be added to the group's proposal
373    /// queue using [`MlsGroup::store_pending_proposal()`](crate::group::mls_group::MlsGroup::store_pending_proposal()).
374    ProposalMessage(Box<QueuedProposal>),
375    /// An [external join proposal](crate::prelude::JoinProposal) sent by a
376    /// [NewMemberProposal](crate::prelude::Sender::NewMemberProposal) sender which is outside the group.
377    ///
378    /// Since this originates from a party outside the group, the [`QueuedProposal`] SHOULD be
379    /// inspected for authorization purposes by the application. If the proposal is deemed to be
380    /// allowed, it should be added to the group's proposal queue using
381    /// [`MlsGroup::store_pending_proposal()`](crate::group::mls_group::MlsGroup::store_pending_proposal()).
382    ExternalJoinProposalMessage(Box<QueuedProposal>),
383    /// A Commit message.
384    ///
385    /// The [`StagedCommit`] can be inspected for authorization purposes by the application.
386    /// If the type of the commit and the proposals it covers are deemed to be allowed,
387    /// the commit should be merged into the group's state using
388    /// [`MlsGroup::merge_staged_commit()`](crate::group::mls_group::MlsGroup::merge_staged_commit()).
389    StagedCommitMessage(Box<StagedCommit>),
390}
391
392/// Application message received through a [ProcessedMessage].
393#[derive(Debug, PartialEq, Eq)]
394pub struct ApplicationMessage {
395    bytes: Vec<u8>,
396}
397
398impl ApplicationMessage {
399    /// Create a new [ApplicationMessage].
400    pub(crate) fn new(bytes: Vec<u8>) -> Self {
401        Self { bytes }
402    }
403
404    /// Returns the inner bytes and consumes the [`ApplicationMessage`].
405    pub fn into_bytes(self) -> Vec<u8> {
406        self.bytes
407    }
408}