Skip to main content

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