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