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::{
34 errors::ValidationError, mls_group::staged_commit::StagedCommit, LeafNodeLifetimePolicy,
35 },
36 tree::{
37 secret_tree::{DecryptionSecret, PendingRatchetUpdate, SecretType},
38 sender_ratchet::SenderRatchetConfiguration,
39 },
40 versions::ProtocolVersion,
41};
42
43#[cfg(feature = "extensions-draft")]
44use crate::{
45 component::ComponentId,
46 framing::safe_aad::SafeAad,
47 group::{
48 mls_group::{errors::ResolveAppDataCommitError, processing::UnresolvedAppDataCommit},
49 ExportedSecret, StagedCommitSafeExport,
50 },
51};
52
53#[cfg(feature = "virtual-clients-draft")]
54use crate::components::vc_commit_data::{VcCommitDataError, VirtualClientCommitData};
55
56use super::{
57 mls_auth_content::AuthenticatedContent,
58 mls_auth_content_in::{AuthenticatedContentIn, VerifiableAuthenticatedContentIn},
59 private_message_in::PrivateMessageIn,
60 public_message_in::PublicMessageIn,
61 *,
62};
63
64/// Result of decrypting an inbound PrivateMessage: either content this client
65/// can process further, or a message this client authored itself, which it
66/// cannot decrypt.
67#[derive(Debug)]
68pub(crate) enum InboundDecryptionResult {
69 /// A message from another sender or from a sibling emulator client (with
70 /// the `virtual-clients-draft` feature), decrypted and ready for parsing.
71 Decrypted {
72 message: DecryptedMessage,
73 /// The sender ratchet change that decrypting a PrivateMessage requires.
74 /// It must only be applied once the message is processed successfully.
75 /// `None` for a PublicMessage.
76 ratchet_update: Option<PendingRatchetUpdate>,
77 },
78 /// A private message whose sender data claims this client's own leaf.
79 /// Carries the plaintext framing fields needed to build the
80 /// [`ProcessedMessage`], since the content itself cannot be decrypted.
81 OwnPrivateMessage {
82 epoch: GroupEpoch,
83 authenticated_data: Vec<u8>,
84 },
85}
86
87impl InboundDecryptionResult {
88 /// Returns the decrypted message, or `None` for an own private message.
89 #[cfg(test)]
90 pub(crate) fn into_decrypted(self) -> Option<DecryptedMessage> {
91 match self {
92 Self::Decrypted { message, .. } => Some(message),
93 Self::OwnPrivateMessage { .. } => None,
94 }
95 }
96}
97
98/// Intermediate message that can be constructed either from a public message or from private message.
99/// If it it constructed from a ciphertext message, the ciphertext message is decrypted first.
100/// This function implements the following checks:
101/// - ValSem005
102/// - ValSem007
103/// - ValSem009
104#[derive(Debug)]
105pub(crate) struct DecryptedMessage {
106 verifiable_content: VerifiableAuthenticatedContentIn,
107 /// Recovered sender emulation-group leaf index for an application
108 /// message from a sibling emulator client.
109 #[cfg(feature = "virtual-clients-draft")]
110 emulator_sender_leaf_index: Option<LeafNodeIndex>,
111}
112
113impl DecryptedMessage {
114 /// Constructs a [DecryptedMessage] from a [VerifiableAuthenticatedContent].
115 pub(crate) fn from_inbound_public_message<'a>(
116 public_message: PublicMessageIn,
117 message_secrets_option: impl Into<Option<&'a MessageSecrets>>,
118 serialized_context: Vec<u8>,
119 crypto: &impl OpenMlsCrypto,
120 ciphersuite: Ciphersuite,
121 ) -> Result<Self, ValidationError> {
122 if public_message.sender().is_member() {
123 // ValSem007 Membership tag presence
124 if public_message.membership_tag().is_none() {
125 return Err(ValidationError::MissingMembershipTag);
126 }
127
128 if let Some(message_secrets) = message_secrets_option.into() {
129 // Verify the membership tag. This needs to be done explicitly for PublicMessage messages,
130 // it is implicit for PrivateMessage messages (because the encryption can only be known by members).
131 // ValSem008
132 // https://validation.openmls.tech/#valn1302
133 public_message.verify_membership(
134 crypto,
135 ciphersuite,
136 message_secrets.membership_key(),
137 message_secrets.serialized_context(),
138 )?;
139 }
140 }
141
142 let verifiable_content = public_message.into_verifiable_content(serialized_context);
143
144 // Public messages don't carry a reuse_guard, so no emulator
145 // sender leaf index to recover.
146 Self::from_verifiable_content(
147 verifiable_content,
148 #[cfg(feature = "virtual-clients-draft")]
149 None,
150 )
151 }
152
153 /// Constructs a [DecryptedMessage] from a [PrivateMessage] by attempting to decrypt it
154 /// to a [VerifiableAuthenticatedContent] first.
155 pub(crate) fn from_inbound_ciphertext(
156 ciphertext: PrivateMessageIn,
157 crypto: &impl OpenMlsCrypto,
158 group: &mut MlsGroup,
159 sender_ratchet_configuration: &SenderRatchetConfiguration,
160 #[cfg(feature = "virtual-clients-draft")] emulator_ctx: Option<
161 &crate::framing::private_message::EmulatorReuseGuardCtx<'_>,
162 >,
163 ) -> Result<InboundDecryptionResult, ValidationError> {
164 // This will be refactored with #265.
165 let ciphersuite = group.ciphersuite();
166 // TODO: #819 The old leaves should not be needed any more.
167 // Revisit when the transition is further along.
168 let (message_secrets, _old_leaves) = group
169 .message_secrets_and_leaves(ciphertext.epoch())
170 .map_err(MessageDecryptionError::SecretTreeError)?;
171 let own_index = message_secrets.own_index();
172 let sender_data = ciphertext.sender_data(message_secrets, crypto, ciphersuite)?;
173 let own_sender = sender_data.leaf_index == own_index;
174 // If we are the sender, the content cannot be decrypted and the
175 // signature cannot be verified: the own sender ratchet only produces
176 // encryption keys. Return early before touching any ratchet state so
177 // no decryption counter is consumed and no spurious "generation out
178 // of bounds" error is logged for an own echo.
179 //
180 // With the `virtual-clients-draft` feature, own-leaf messages are only
181 // decryptable when there is an emulator context for this epoch: a
182 // sibling emulator client shares the leaf, and the dual-use ratchet
183 // retains the secrets of unconfirmed own sends. In that case we still
184 // look up the secret below. The message only surfaces as an own
185 // private message when the lookup finds that this client confirmed
186 // the send.
187 //
188 // Without an emulator context the group does not use virtual clients
189 // (which is the case for the emulation group) so an own message is
190 // unambiguously our own echo and we short-circuit just like the non-VC
191 // path.
192 #[cfg(not(feature = "virtual-clients-draft"))]
193 let short_circuit_own = own_sender;
194 #[cfg(feature = "virtual-clients-draft")]
195 let short_circuit_own = own_sender && emulator_ctx.is_none();
196 if short_circuit_own {
197 return Ok(InboundDecryptionResult::OwnPrivateMessage {
198 epoch: ciphertext.epoch(),
199 authenticated_data: ciphertext.aad().to_vec(),
200 });
201 }
202 #[cfg(feature = "virtual-clients-draft")]
203 let effective_emulator_ctx = match emulator_ctx {
204 Some(ctx) if own_sender => Some(ctx),
205 _ => None,
206 };
207 let message_secrets = group
208 .message_secrets_for_epoch_mut(ciphertext.epoch())
209 .map_err(|_| MessageDecryptionError::AeadError)?;
210 let generation = sender_data.generation;
211 let decryption_secret = message_secrets.secret_tree_mut().prepare_decryption(
212 ciphersuite,
213 crypto,
214 sender_data.leaf_index,
215 SecretType::from(&ciphertext.content_type()),
216 generation,
217 sender_ratchet_configuration,
218 );
219 let (ratchet_key_material, ratchet_update) = match decryption_secret {
220 Ok((DecryptionSecret::Available(ratchet_key_material), ratchet_update)) => {
221 (ratchet_key_material, ratchet_update)
222 }
223 #[cfg(feature = "virtual-clients-draft")]
224 Ok((DecryptionSecret::OwnMessageConfirmed, _)) => {
225 log::debug!(" Own generation {generation} was already confirmed.");
226 return Ok(InboundDecryptionResult::OwnPrivateMessage {
227 epoch: ciphertext.epoch(),
228 authenticated_data: ciphertext.aad().to_vec(),
229 });
230 }
231 Err(e) => {
232 log::error!(" Ciphertext generation out of bounds {generation}\n\t{e:?}");
233 return Err(MessageDecryptionError::SecretTreeError(e).into());
234 }
235 };
236 let decrypted = ciphertext.to_verifiable_content(
237 crypto,
238 message_secrets,
239 ratchet_key_material,
240 sender_data,
241 #[cfg(feature = "virtual-clients-draft")]
242 effective_emulator_ctx,
243 )?;
244 let message = Self::from_verifiable_content(
245 decrypted.verifiable,
246 #[cfg(feature = "virtual-clients-draft")]
247 decrypted.emulator_sender_leaf_index,
248 )?;
249 Ok(InboundDecryptionResult::Decrypted {
250 message,
251 ratchet_update,
252 })
253 }
254
255 // Internal constructor function. Does the following checks:
256 // - Confirmation tag must be present for Commit messages
257 // - Membership tag must be present for member messages, if the original incoming message was not an PrivateMessage
258 // - Ensures application messages were originally PrivateMessage messages
259 fn from_verifiable_content(
260 verifiable_content: VerifiableAuthenticatedContentIn,
261 #[cfg(feature = "virtual-clients-draft")] emulator_sender_leaf_index: Option<LeafNodeIndex>,
262 ) -> Result<Self, ValidationError> {
263 // ValSem009
264 if verifiable_content.content_type() == ContentType::Commit
265 && verifiable_content.confirmation_tag().is_none()
266 {
267 return Err(ValidationError::MissingConfirmationTag);
268 }
269 // ValSem005
270 if verifiable_content.content_type() == ContentType::Application {
271 if verifiable_content.wire_format() != WireFormat::PrivateMessage {
272 return Err(ValidationError::UnencryptedApplicationMessage);
273 } else if !verifiable_content.sender().is_member() {
274 // This should not happen because the sender of an PrivateMessage should always be a member
275 return Err(LibraryError::custom("Expected sender to be member.").into());
276 }
277 }
278 Ok(DecryptedMessage {
279 verifiable_content,
280 #[cfg(feature = "virtual-clients-draft")]
281 emulator_sender_leaf_index,
282 })
283 }
284
285 /// Recovered sender emulation-group leaf index, if the message came
286 /// from a sibling emulator client.
287 #[cfg(feature = "virtual-clients-draft")]
288 #[allow(dead_code)]
289 pub(crate) fn emulator_sender_leaf_index(&self) -> Option<LeafNodeIndex> {
290 self.emulator_sender_leaf_index
291 }
292
293 /// Gets the correct credential from the message depending on the sender type.
294 ///
295 /// The closure argument is used to look up the credential and signature key. If the epoch of
296 /// the message is the same as that of the group, look it up in the tree; else, look in up in
297 /// the past trees of the message secret store.
298 ///
299 /// Checks the following semantic validation:
300 /// - ValSem112
301 /// - ValSem245
302 /// - Prepares ValSem246 by setting the right credential. The remainder
303 /// of ValSem246 is validated as part of ValSem010.
304 /// - [valn1301](https://validation.openmls.tech/#valn1301)
305 ///
306 /// Returns the [`Credential`] and the leaf's [`SignaturePublicKey`].
307 pub(crate) fn credential(
308 &self,
309 look_up_credential_with_key: impl Fn(LeafNodeIndex) -> Option<CredentialWithKey>,
310 external_senders: Option<&ExternalSendersExtension>,
311 ) -> Result<CredentialWithKey, ValidationError> {
312 let sender = self.sender();
313 match sender {
314 Sender::Member(leaf_index) => {
315 // https://validation.openmls.tech/#valn1306
316 look_up_credential_with_key(*leaf_index).ok_or(ValidationError::UnknownMember)
317 }
318 Sender::External(index) => {
319 let sender = external_senders
320 .ok_or(ValidationError::NoExternalSendersExtension)?
321 .get(index.index())
322 .ok_or(ValidationError::UnauthorizedExternalSender)?;
323 Ok(CredentialWithKey {
324 credential: sender.credential().clone(),
325 signature_key: sender.signature_key().clone(),
326 })
327 }
328 Sender::NewMemberCommit | Sender::NewMemberProposal => {
329 // Fetch the credential from the message itself.
330 // https://validation.openmls.tech/#valn0407
331 self.verifiable_content.new_member_credential()
332 }
333 }
334 }
335
336 /// Returns the sender.
337 pub fn sender(&self) -> &Sender {
338 self.verifiable_content.sender()
339 }
340
341 /// Returns the [`VerifiableAuthenticatedContent`].
342 pub(crate) fn verifiable_content(&self) -> &VerifiableAuthenticatedContentIn {
343 &self.verifiable_content
344 }
345}
346
347/// Result of [`UnverifiedMessage::verify`].
348pub(crate) struct VerifiedMessage {
349 pub(crate) content: AuthenticatedContent,
350 pub(crate) credential: Credential,
351 #[cfg(feature = "virtual-clients-draft")]
352 pub(crate) emulator_sender_leaf_index: Option<LeafNodeIndex>,
353}
354
355/// Context that is needed to verify the signature of a the leaf node of an
356/// UpdatePath or an update proposal.
357#[derive(Debug, Clone)]
358pub(crate) enum SenderContext {
359 Member((GroupId, LeafNodeIndex)),
360 ExternalCommit {
361 group_id: GroupId,
362 leftmost_blank_index: LeafNodeIndex,
363 self_removes_in_store: Vec<SelfRemoveInStore>,
364 },
365}
366
367/// Partially checked and potentially decrypted message (if it was originally encrypted).
368/// Use this to inspect the [`Credential`] of the message sender
369/// and the optional `aad` if the original message was encrypted.
370/// The [`OpenMlsSignaturePublicKey`] is used to verify the signature of the
371/// message.
372#[derive(Debug, Clone)]
373pub struct UnverifiedMessage {
374 verifiable_content: VerifiableAuthenticatedContentIn,
375 credential: Credential,
376 sender_pk: OpenMlsSignaturePublicKey,
377 sender_context: Option<SenderContext>,
378 /// See [`DecryptedMessage::emulator_sender_leaf_index`].
379 #[cfg(feature = "virtual-clients-draft")]
380 emulator_sender_leaf_index: Option<LeafNodeIndex>,
381}
382
383impl UnverifiedMessage {
384 /// Construct an [UnverifiedMessage] from a [DecryptedMessage] and an optional [Credential].
385 pub(crate) fn from_decrypted_message(
386 decrypted_message: DecryptedMessage,
387 credential: Credential,
388 sender_pk: OpenMlsSignaturePublicKey,
389 sender_context: Option<SenderContext>,
390 ) -> Self {
391 #[cfg(feature = "virtual-clients-draft")]
392 let emulator_sender_leaf_index = decrypted_message.emulator_sender_leaf_index;
393 UnverifiedMessage {
394 verifiable_content: decrypted_message.verifiable_content,
395 credential,
396 sender_pk,
397 sender_context,
398 #[cfg(feature = "virtual-clients-draft")]
399 emulator_sender_leaf_index,
400 }
401 }
402
403 /// Verify the [`UnverifiedMessage`].
404 pub(crate) fn verify(
405 self,
406 ciphersuite: Ciphersuite,
407 crypto: &impl OpenMlsCrypto,
408 protocol_version: ProtocolVersion,
409 lifetime_policy: LeafNodeLifetimePolicy,
410 ) -> Result<VerifiedMessage, ValidationError> {
411 let content: AuthenticatedContentIn = self
412 .verifiable_content
413 .verify(crypto, &self.sender_pk)
414 .map_err(|_| ValidationError::InvalidSignature)?;
415 // https://validation.openmls.tech/#valn1302
416 // https://validation.openmls.tech/#valn1304
417 let content = content.validate(
418 ciphersuite,
419 crypto,
420 self.sender_context,
421 protocol_version,
422 lifetime_policy,
423 )?;
424 Ok(VerifiedMessage {
425 content,
426 credential: self.credential,
427 #[cfg(feature = "virtual-clients-draft")]
428 emulator_sender_leaf_index: self.emulator_sender_leaf_index,
429 })
430 }
431}
432
433/// A message that has passed all syntax and semantics checks.
434#[derive(Debug)]
435pub struct ProcessedMessage {
436 group_id: GroupId,
437 epoch: GroupEpoch,
438 sender: Sender,
439 authenticated_data: Vec<u8>,
440 content: ProcessedMessageContent,
441 credential: Credential,
442 /// See [`Self::emulator_sender_leaf_index`].
443 #[cfg(feature = "virtual-clients-draft")]
444 emulator_sender_leaf_index: Option<LeafNodeIndex>,
445 /// Parsed Safe AAD prefix, populated only when the message's GroupContext
446 /// required Safe AAD framing. `None` otherwise.
447 #[cfg(feature = "extensions-draft")]
448 safe_aad: Option<SafeAad>,
449 /// Length in bytes of the Safe AAD prefix at the start of
450 /// `authenticated_data`. Zero when [`Self::safe_aad`] is `None`.
451 #[cfg(feature = "extensions-draft")]
452 safe_aad_prefix_len: usize,
453}
454
455impl ProcessedMessage {
456 /// Create a new `ProcessedMessage`.
457 pub(crate) fn new(
458 group_id: GroupId,
459 epoch: GroupEpoch,
460 sender: Sender,
461 authenticated_data: Vec<u8>,
462 content: ProcessedMessageContent,
463 credential: Credential,
464 #[cfg(feature = "virtual-clients-draft")] emulator_sender_leaf_index: Option<LeafNodeIndex>,
465 ) -> Self {
466 Self {
467 group_id,
468 epoch,
469 sender,
470 authenticated_data,
471 content,
472 credential,
473 #[cfg(feature = "virtual-clients-draft")]
474 emulator_sender_leaf_index,
475 #[cfg(feature = "extensions-draft")]
476 safe_aad: None,
477 #[cfg(feature = "extensions-draft")]
478 safe_aad_prefix_len: 0,
479 }
480 }
481
482 /// Splits off the [`UnresolvedAppDataCommit`] this message carries so the
483 /// caller can stage it. The returned function puts the [`StagedCommit`]
484 /// back as [`ProcessedMessageContent::StagedCommitMessage`] content and
485 /// keeps all other fields (sender, credential, authenticated data, Safe AAD
486 /// state) intact. Staging may be sync or async, so we return the pieces
487 /// rather than take a staging closure.
488 ///
489 /// Returns an error if the content is not an unresolved app data commit.
490 /// The message is consumed either way.
491 #[cfg(feature = "extensions-draft")]
492 pub(crate) fn split_unresolved_app_data_commit(
493 self,
494 ) -> Result<
495 (UnresolvedAppDataCommit, impl FnOnce(StagedCommit) -> Self),
496 ResolveAppDataCommitError,
497 > {
498 let ProcessedMessageContent::UnresolvedAppDataCommit(unresolved_commit) = self.content
499 else {
500 return Err(ResolveAppDataCommitError::NotAnUnresolvedAppDataCommit);
501 };
502 let with_staged_commit = move |staged_commit: StagedCommit| Self {
503 content: ProcessedMessageContent::StagedCommitMessage(Box::new(staged_commit)),
504 ..self
505 };
506 Ok((*unresolved_commit, with_staged_commit))
507 }
508
509 /// Parse the Safe AAD prefix at the start of `authenticated_data` and
510 /// attach it to this message. Callers should invoke this only when the receiving
511 /// group's GroupContext requires Safe AAD framing. Otherwise, `safe_aad`
512 /// stays `None` and `authenticated_data` is the caller-supplied bytes
513 /// untouched.
514 #[cfg(feature = "extensions-draft")]
515 pub(crate) fn try_attach_safe_aad(&mut self) -> Result<(), crate::framing::SafeAadError> {
516 let (safe_aad, prefix_len) =
517 crate::framing::safe_aad::parse_authenticated_data_prefix(&self.authenticated_data)?;
518 self.safe_aad = Some(safe_aad);
519 self.safe_aad_prefix_len = prefix_len;
520 Ok(())
521 }
522
523 /// Returns the parsed Safe AAD struct, or `None` if Safe AAD was not
524 /// active for the group this message belongs to.
525 #[cfg(feature = "extensions-draft")]
526 pub fn safe_aad(&self) -> Option<&SafeAad> {
527 self.safe_aad.as_ref()
528 }
529
530 /// Look up a Safe AAD item by [`ComponentId`].
531 #[cfg(feature = "extensions-draft")]
532 pub fn safe_aad_item(&self, component_id: crate::component::ComponentId) -> Option<&[u8]> {
533 self.safe_aad
534 .as_ref()
535 .and_then(|safe_aad| safe_aad.get(component_id))
536 }
537
538 /// Parse the virtual-clients commit data from this message's Safe AAD.
539 ///
540 /// Returns `Ok(None)` when the message carries no Safe AAD item under
541 /// [`VC_COMPONENT_ID`], which includes the case where Safe AAD was not
542 /// active for the group.
543 ///
544 /// [`VC_COMPONENT_ID`]: crate::components::vc_derivation_info::VC_COMPONENT_ID
545 #[cfg(feature = "virtual-clients-draft")]
546 pub fn vc_commit_data(&self) -> Result<Option<VirtualClientCommitData>, VcCommitDataError> {
547 let Some(safe_aad) = self.safe_aad.as_ref() else {
548 return Ok(None);
549 };
550 VirtualClientCommitData::from_safe_aad(safe_aad)
551 }
552
553 /// Returns the bytes of `authenticated_data` after any Safe AAD prefix.
554 /// Equal to [`Self::aad`] when no Safe AAD prefix is present.
555 #[cfg(feature = "extensions-draft")]
556 pub fn tail_aad(&self) -> &[u8] {
557 &self.authenticated_data[self.safe_aad_prefix_len..]
558 }
559
560 /// Returns the sender's leaf index in the emulation group when this
561 /// message is an application message from a sibling emulator client.
562 #[cfg(feature = "virtual-clients-draft")]
563 pub fn emulator_sender_leaf_index(&self) -> Option<LeafNodeIndex> {
564 self.emulator_sender_leaf_index
565 }
566
567 /// Returns the group ID of the message.
568 pub fn group_id(&self) -> &GroupId {
569 &self.group_id
570 }
571
572 /// Returns the epoch of the message.
573 pub fn epoch(&self) -> GroupEpoch {
574 self.epoch
575 }
576
577 /// Returns the sender of the message.
578 pub fn sender(&self) -> &Sender {
579 &self.sender
580 }
581
582 /// Returns the additional authenticated data (AAD) of the message.
583 pub fn aad(&self) -> &[u8] {
584 &self.authenticated_data
585 }
586
587 /// Returns the content of the message.
588 pub fn content(&self) -> &ProcessedMessageContent {
589 &self.content
590 }
591
592 /// Returns the content of the message and consumes the message.
593 pub fn into_content(self) -> ProcessedMessageContent {
594 self.content
595 }
596
597 /// Returns the credential of the message.
598 pub fn credential(&self) -> &Credential {
599 &self.credential
600 }
601
602 /// Safely export a value if the content of the processed message is a
603 /// [`StagedCommit`].
604 #[cfg(feature = "extensions-draft")]
605 pub fn safe_export_secret<Crypto: OpenMlsCrypto>(
606 &mut self,
607 crypto: &Crypto,
608 component_id: ComponentId,
609 ) -> Result<ExportedSecret<StagedCommitSafeExport>, ProcessedMessageSafeExportSecretError> {
610 if let ProcessedMessageContent::StagedCommitMessage(ref mut staged_commit) =
611 &mut self.content
612 {
613 let secret = staged_commit.safe_export_secret(crypto, component_id)?;
614 Ok(secret)
615 } else {
616 Err(ProcessedMessageSafeExportSecretError::NotACommit)
617 }
618 }
619}
620
621/// Content of a processed message.
622///
623/// See the content variants' documentation for more information.
624/// [`StagedCommit`] and [`QueuedProposal`] can be inspected for authorization purposes.
625#[derive(Debug)]
626pub enum ProcessedMessageContent {
627 /// An application message.
628 ///
629 /// The [`ApplicationMessage`] contains a vector of bytes that can be used right-away.
630 ApplicationMessage(ApplicationMessage),
631 /// A standalone proposal.
632 ///
633 /// The [`QueuedProposal`] can be inspected for authorization purposes by the application.
634 /// If the proposal is deemed to be allowed, it should be added to the group's proposal
635 /// queue using [`MlsGroup::store_pending_proposal()`](crate::group::mls_group::MlsGroup::store_pending_proposal()).
636 ProposalMessage(Box<QueuedProposal>),
637 /// An [external join proposal](crate::prelude::JoinProposal) sent by a
638 /// [NewMemberProposal](crate::prelude::Sender::NewMemberProposal) sender which is outside the group.
639 ///
640 /// Since this originates from a party outside the group, the [`QueuedProposal`] SHOULD be
641 /// inspected for authorization purposes by the application. If the proposal is deemed to be
642 /// allowed, it should be added to the group's proposal queue using
643 /// [`MlsGroup::store_pending_proposal()`](crate::group::mls_group::MlsGroup::store_pending_proposal()).
644 ExternalJoinProposalMessage(Box<QueuedProposal>),
645 /// A Commit message.
646 ///
647 /// The [`StagedCommit`] can be inspected for authorization purposes by the application.
648 /// If the type of the commit and the proposals it covers are deemed to be allowed,
649 /// the commit should be merged into the group's state using
650 /// [`MlsGroup::merge_staged_commit()`](crate::group::mls_group::MlsGroup::merge_staged_commit()).
651 StagedCommitMessage(Box<StagedCommit>),
652 /// A Commit authored by this client that it got fanned out by the delivery
653 /// service, matching the group's pending commit.
654 ///
655 /// This is returned instead of
656 /// [`StagedCommitMessage`](Self::StagedCommitMessage) when the processed
657 /// Commit was created by this client and matches the group's pending commit.
658 /// Since this client already holds the corresponding pending commit, the
659 /// incoming Commit is not staged. To apply it, merge the pending commit
660 /// using
661 /// [`MlsGroup::merge_pending_commit()`](crate::group::mls_group::MlsGroup::merge_pending_commit()).
662 /// An own Commit that does not match the pending commit is instead returned
663 /// as a [`StagedCommitMessage`](Self::StagedCommitMessage) (if it has no
664 /// UpdatePath) or rejected (if it has an UpdatePath we cannot decrypt).
665 ///
666 /// The match against the pending commit is established by comparing the
667 /// confirmation tag of the incoming Commit against the one stored with the
668 /// pending commit. The message signature has already been verified, which
669 /// authenticates the Commit as ours, and a matching confirmation tag binds
670 /// the confirmed transcript hash of the new epoch. We do not otherwise
671 /// compare the contents of the incoming Commit against the pending commit,
672 /// and the incoming Commit's state is never adopted.
673 ///
674 /// This is only produced for Commits framed as
675 /// [`PublicMessage`](crate::framing::MlsMessageBodyIn::PublicMessage). A
676 /// Commit framed as a
677 /// [`PrivateMessage`](crate::framing::MlsMessageBodyIn::PrivateMessage)
678 /// cannot be decrypted by its own author and instead surfaces as
679 /// [`OwnPrivateMessage`](Self::OwnPrivateMessage). The exception is the
680 /// `virtual-clients-draft` feature, where an own private Commit whose
681 /// encryption secret is still retained (not yet confirmed) decrypts and
682 /// can produce this variant as well. Under that feature the pending-commit
683 /// match is checked before any sibling-commit (virtual clients) material is
684 /// loaded, so an own Commit fanned back by the delivery service surfaces as
685 /// `OwnPendingCommit` without consuming an operation-secret generation from
686 /// the derivation epoch's operation secret tree.
687 OwnPendingCommit,
688 /// A PrivateMessage whose sender data claims this client's own leaf index,
689 /// i.e. a message this client authored that the delivery service fanned
690 /// back.
691 ///
692 /// The content cannot be decrypted (the own sender ratchet is
693 /// encryption-only) and the signature cannot be verified.
694 ///
695 /// Applications should treat this variant as a hint to skip the message.
696 /// The content type of the incoming message (application/proposal/commit)
697 /// is available via `ProtocolMessage::content_type()` before processing,
698 /// and is unauthenticated plaintext in the PrivateMessage framing.
699 ///
700 /// With the `virtual-clients-draft` feature, own-leaf messages are
701 /// decryptable while their secrets are retained: unconfirmed own sends
702 /// and messages from sibling emulator clients decrypt and process
703 /// normally. This variant is then returned in two cases:
704 ///
705 /// - The group does not use virtual clients, i.e. no derivation epoch
706 /// state is registered for the message's epoch.
707 /// - This client confirmed its own send at the message's generation via
708 /// `MlsGroup::confirm_application_message()` or
709 /// `MlsGroup::confirm_handshake_message()`, so the message is the echo
710 /// of that send.
711 ///
712 /// The second case relies on the Delivery Service rejecting a second
713 /// message at a generation it already accepted from this leaf (see the
714 /// `generation_id` returned when creating unconfirmed messages). Other
715 /// own-leaf messages that cannot be decrypted fail instead. A generation
716 /// already used for a decryption attempt fails with
717 /// [`SecretTreeError::SecretReuseError`](crate::framing::errors::SecretTreeError::SecretReuseError),
718 /// like a duplicate from any other member, since that attempt may not
719 /// have succeeded. A generation that fell out of the receive window fails
720 /// with
721 /// [`SecretTreeError::TooDistantInThePast`](crate::framing::errors::SecretTreeError::TooDistantInThePast),
722 /// since it may be a sibling's message that was never processed.
723 OwnPrivateMessage,
724 /// A Commit message covering AppDataUpdate proposals.
725 ///
726 /// The proposals carry diffs in an application-defined format, so the
727 /// commit cannot be staged before the application has interpreted them and
728 /// computed the resulting dictionary entries. Inspect the proposals via
729 /// [`UnresolvedAppDataCommit::app_data_update_proposals()`], compute the
730 /// updates with the help of
731 /// [`MlsGroup::app_data_dictionary_updater()`](crate::group::mls_group::MlsGroup::app_data_dictionary_updater)
732 /// and resume staging via
733 /// [`MlsGroup::stage_app_data_commit()`](crate::group::mls_group::MlsGroup::stage_app_data_commit).
734 ///
735 /// This variant is likewise returned by
736 /// [`PublicGroup::process_message()`](crate::group::public_group::PublicGroup::process_message),
737 /// where the updates are computed with
738 /// [`PublicGroup::app_data_dictionary_updater()`](crate::group::public_group::PublicGroup::app_data_dictionary_updater)
739 /// and staging resumes via
740 /// [`PublicGroup::stage_app_data_commit()`](crate::group::public_group::PublicGroup::stage_app_data_commit).
741 #[cfg(feature = "extensions-draft")]
742 UnresolvedAppDataCommit(Box<UnresolvedAppDataCommit>),
743}
744
745/// Application message received through a [ProcessedMessage].
746#[derive(Debug, PartialEq, Eq)]
747pub struct ApplicationMessage {
748 bytes: Vec<u8>,
749}
750
751impl ApplicationMessage {
752 /// Create a new [ApplicationMessage].
753 pub(crate) fn new(bytes: Vec<u8>) -> Self {
754 Self { bytes }
755 }
756
757 /// Returns the inner bytes and consumes the [`ApplicationMessage`].
758 pub fn into_bytes(self) -> Vec<u8> {
759 self.bytes
760 }
761}