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