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}