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}