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::{
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}