Skip to main content

mls_rs/group/
message_processor.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// Copyright by contributors to this project.
3// SPDX-License-Identifier: (Apache-2.0 OR MIT)
4
5#[cfg(all(
6    feature = "by_ref_proposal",
7    feature = "custom_proposal",
8    feature = "self_remove_proposal"
9))]
10use super::SelfRemoveProposal;
11use super::{
12    commit_sender,
13    confirmation_tag::ConfirmationTag,
14    framing::{
15        ApplicationData, Content, ContentType, MlsMessage, MlsMessagePayload, PublicMessage, Sender,
16    },
17    message_signature::AuthenticatedContent,
18    mls_rules::{CommitDirection, MlsRules},
19    proposal_filter::ProposalBundle,
20    state::GroupState,
21    transcript_hash::InterimTranscriptHash,
22    transcript_hashes, validate_group_info_member, GroupContext, GroupInfo, ReInitProposal,
23    RemoveProposal, Welcome,
24};
25use crate::{
26    client::MlsError,
27    group::proposal_filter::path_update_required,
28    key_package::validate_key_package_properties,
29    time::MlsTime,
30    tree_kem::{
31        leaf_node_validator::{LeafNodeValidator, ValidationContext},
32        node::LeafIndex,
33        path_secret::PathSecret,
34        validate_update_path, TreeKemPrivate, TreeKemPublic, ValidatedUpdatePath,
35    },
36    CipherSuiteProvider, KeyPackage,
37};
38use itertools::Itertools;
39use mls_rs_codec::{MlsDecode, MlsEncode, MlsSize};
40
41use alloc::boxed::Box;
42use alloc::vec::Vec;
43use core::fmt::{self, Debug};
44use mls_rs_core::{
45    identity::{IdentityProvider, MemberValidationContext},
46    protocol_version::ProtocolVersion,
47    psk::PreSharedKeyStorage,
48};
49
50#[cfg(feature = "by_ref_proposal")]
51use super::proposal_ref::ProposalRef;
52
53#[cfg(not(feature = "by_ref_proposal"))]
54use crate::group::proposal_cache::resolve_for_commit;
55
56use super::proposal::Proposal;
57use super::proposal_filter::ProposalInfo;
58
59#[cfg(feature = "private_message")]
60use crate::group::framing::PrivateMessage;
61
62#[derive(Debug)]
63pub(crate) struct ProvisionalState {
64    pub(crate) public_tree: TreeKemPublic,
65    pub(crate) applied_proposals: ProposalBundle,
66    pub(crate) group_context: GroupContext,
67    pub(crate) external_init_index: Option<LeafIndex>,
68    pub(crate) indexes_of_added_kpkgs: Vec<LeafIndex>,
69    pub(crate) unused_proposals: Vec<ProposalInfo<Proposal>>,
70}
71
72#[derive(Clone, Debug, PartialEq, MlsSize, MlsEncode, MlsDecode)]
73#[non_exhaustive]
74pub struct NewEpoch {
75    pub epoch: u64,
76    pub prior_state: GroupState,
77    pub applied_proposals: Vec<ProposalInfo<Proposal>>,
78    pub unused_proposals: Vec<ProposalInfo<Proposal>>,
79}
80
81impl NewEpoch {
82    pub(crate) fn new(prior_state: GroupState, provisional_state: &ProvisionalState) -> NewEpoch {
83        NewEpoch {
84            epoch: provisional_state.group_context.epoch,
85            prior_state,
86            unused_proposals: provisional_state.unused_proposals.clone(),
87            applied_proposals: provisional_state
88                .applied_proposals
89                .clone()
90                .into_proposals()
91                .collect_vec(),
92        }
93    }
94}
95
96impl NewEpoch {
97    pub fn epoch(&self) -> u64 {
98        self.epoch
99    }
100
101    pub fn prior_state(&self) -> &GroupState {
102        &self.prior_state
103    }
104
105    pub fn applied_proposals(&self) -> &[ProposalInfo<Proposal>] {
106        &self.applied_proposals
107    }
108
109    pub fn unused_proposals(&self) -> &[ProposalInfo<Proposal>] {
110        &self.unused_proposals
111    }
112}
113
114#[derive(Clone, Debug, PartialEq)]
115pub enum CommitEffect {
116    NewEpoch(Box<NewEpoch>),
117    Removed {
118        new_epoch: Box<NewEpoch>,
119        remover: Sender,
120    },
121    ReInit(ProposalInfo<ReInitProposal>),
122}
123
124impl MlsSize for CommitEffect {
125    fn mls_encoded_len(&self) -> usize {
126        0u8.mls_encoded_len()
127            + match self {
128                Self::NewEpoch(e) => e.mls_encoded_len(),
129                Self::Removed { new_epoch, remover } => {
130                    new_epoch.mls_encoded_len() + remover.mls_encoded_len()
131                }
132                Self::ReInit(r) => r.mls_encoded_len(),
133            }
134    }
135}
136
137impl MlsEncode for CommitEffect {
138    fn mls_encode(&self, writer: &mut Vec<u8>) -> Result<(), mls_rs_codec::Error> {
139        match self {
140            Self::NewEpoch(e) => {
141                1u8.mls_encode(writer)?;
142                e.mls_encode(writer)?;
143            }
144            Self::Removed { new_epoch, remover } => {
145                2u8.mls_encode(writer)?;
146                new_epoch.mls_encode(writer)?;
147                remover.mls_encode(writer)?;
148            }
149            Self::ReInit(r) => {
150                3u8.mls_encode(writer)?;
151                r.mls_encode(writer)?;
152            }
153        }
154
155        Ok(())
156    }
157}
158
159impl MlsDecode for CommitEffect {
160    fn mls_decode(reader: &mut &[u8]) -> Result<Self, mls_rs_codec::Error> {
161        match u8::mls_decode(reader)? {
162            1u8 => Ok(Self::NewEpoch(NewEpoch::mls_decode(reader)?.into())),
163            2u8 => Ok(Self::Removed {
164                new_epoch: NewEpoch::mls_decode(reader)?.into(),
165                remover: Sender::mls_decode(reader)?,
166            }),
167            3u8 => Ok(Self::ReInit(ProposalInfo::mls_decode(reader)?)),
168            _ => Err(mls_rs_codec::Error::UnsupportedEnumDiscriminant),
169        }
170    }
171}
172
173#[derive(Debug, Clone)]
174#[allow(clippy::large_enum_variant)]
175/// An event generated as a result of processing a message for a group with
176/// [`Group::process_incoming_message`](crate::group::Group::process_incoming_message).
177pub enum ReceivedMessage {
178    /// An application message was decrypted.
179    ApplicationMessage(ApplicationMessageDescription),
180    /// A new commit was processed creating a new group state.
181    Commit(CommitMessageDescription),
182    /// A proposal was received.
183    Proposal(ProposalMessageDescription),
184    /// Validated GroupInfo object
185    GroupInfo(GroupInfo),
186    /// Validated welcome message
187    Welcome,
188    /// Validated key package
189    KeyPackage(KeyPackage),
190}
191
192impl TryFrom<ApplicationMessageDescription> for ReceivedMessage {
193    type Error = MlsError;
194
195    fn try_from(value: ApplicationMessageDescription) -> Result<Self, Self::Error> {
196        Ok(ReceivedMessage::ApplicationMessage(value))
197    }
198}
199
200impl From<CommitMessageDescription> for ReceivedMessage {
201    fn from(value: CommitMessageDescription) -> Self {
202        ReceivedMessage::Commit(value)
203    }
204}
205
206impl From<ProposalMessageDescription> for ReceivedMessage {
207    fn from(value: ProposalMessageDescription) -> Self {
208        ReceivedMessage::Proposal(value)
209    }
210}
211
212impl From<GroupInfo> for ReceivedMessage {
213    fn from(value: GroupInfo) -> Self {
214        ReceivedMessage::GroupInfo(value)
215    }
216}
217
218impl From<Welcome> for ReceivedMessage {
219    fn from(_: Welcome) -> Self {
220        ReceivedMessage::Welcome
221    }
222}
223
224impl From<KeyPackage> for ReceivedMessage {
225    fn from(value: KeyPackage) -> Self {
226        ReceivedMessage::KeyPackage(value)
227    }
228}
229
230#[derive(Clone, PartialEq, Eq)]
231/// Description of a MLS application message.
232pub struct ApplicationMessageDescription {
233    /// Index of this user in the group state.
234    pub sender_index: u32,
235    /// Received application data.
236    data: ApplicationData,
237    /// Plaintext authenticated data in the received MLS packet.
238    pub authenticated_data: Vec<u8>,
239    /// Unauthenticated key generation used to decrypt the message. See documentation for
240    /// [`Group::peek_next_key_generation`] for usage.
241    #[cfg(all(feature = "export_key_generation", feature = "private_message"))]
242    pub unauthenticated_key_generation: Option<u32>,
243}
244
245impl Debug for ApplicationMessageDescription {
246    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247        let mut res = f.debug_struct("ApplicationMessageDescription");
248        res.field("sender_index", &self.sender_index)
249            .field("data", &self.data)
250            .field(
251                "authenticated_data",
252                &mls_rs_core::debug::pretty_bytes(&self.authenticated_data),
253            );
254        #[cfg(all(feature = "export_key_generation", feature = "private_message"))]
255        res.field(
256            "unauthenticated_key_generation",
257            &self.unauthenticated_key_generation,
258        );
259        res.finish()
260    }
261}
262
263impl ApplicationMessageDescription {
264    pub fn data(&self) -> &[u8] {
265        self.data.as_bytes()
266    }
267}
268
269#[derive(Clone, PartialEq, MlsSize, MlsEncode, MlsDecode)]
270#[non_exhaustive]
271/// Description of a processed MLS commit message.
272pub struct CommitMessageDescription {
273    /// True if this is the result of an external commit.
274    pub is_external: bool,
275    /// The index in the group state of the member who performed this commit.
276    pub committer: u32,
277    /// A full description of group state changes as a result of this commit.
278    pub effect: CommitEffect,
279    /// Plaintext authenticated data in the received MLS packet.
280    #[mls_codec(with = "mls_rs_codec::byte_vec")]
281    pub authenticated_data: Vec<u8>,
282}
283
284impl Debug for CommitMessageDescription {
285    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
286        f.debug_struct("CommitMessageDescription")
287            .field("is_external", &self.is_external)
288            .field("committer", &self.committer)
289            .field("effect", &self.effect)
290            .field(
291                "authenticated_data",
292                &mls_rs_core::debug::pretty_bytes(&self.authenticated_data),
293            )
294            .finish()
295    }
296}
297
298#[derive(Debug, Clone, Copy, PartialEq, Eq, MlsEncode, MlsDecode, MlsSize)]
299#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
300#[repr(u8)]
301/// Proposal sender type.
302pub enum ProposalSender {
303    /// A current member of the group by index in the group state.
304    Member(u32) = 1u8,
305    /// An external entity by index within an
306    /// [`ExternalSendersExt`](crate::extension::built_in::ExternalSendersExt).
307    External(u32) = 2u8,
308    /// A new member proposing their addition to the group.
309    NewMember = 3u8,
310}
311
312impl TryFrom<Sender> for ProposalSender {
313    type Error = MlsError;
314
315    fn try_from(value: Sender) -> Result<Self, Self::Error> {
316        match value {
317            Sender::Member(index) => Ok(Self::Member(index)),
318            #[cfg(feature = "by_ref_proposal")]
319            Sender::External(index) => Ok(Self::External(index)),
320            #[cfg(feature = "by_ref_proposal")]
321            Sender::NewMemberProposal => Ok(Self::NewMember),
322            Sender::NewMemberCommit => Err(MlsError::InvalidSender),
323        }
324    }
325}
326
327#[cfg(feature = "by_ref_proposal")]
328#[derive(Clone, MlsEncode, MlsDecode, MlsSize, PartialEq)]
329#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
330#[non_exhaustive]
331/// Description of a processed MLS proposal message.
332pub struct ProposalMessageDescription {
333    /// Sender of the proposal.
334    pub sender: ProposalSender,
335    /// Proposal content.
336    pub proposal: Proposal,
337    /// Plaintext authenticated data in the received MLS packet.
338    pub authenticated_data: Vec<u8>,
339    /// Proposal reference.
340    pub proposal_ref: ProposalRef,
341}
342
343#[cfg(feature = "by_ref_proposal")]
344impl Debug for ProposalMessageDescription {
345    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
346        f.debug_struct("ProposalMessageDescription")
347            .field("sender", &self.sender)
348            .field("proposal", &self.proposal)
349            .field(
350                "authenticated_data",
351                &mls_rs_core::debug::pretty_bytes(&self.authenticated_data),
352            )
353            .field("proposal_ref", &self.proposal_ref)
354            .finish()
355    }
356}
357
358#[cfg(feature = "by_ref_proposal")]
359#[derive(MlsSize, MlsEncode, MlsDecode)]
360pub struct CachedProposal {
361    pub(crate) proposal: Proposal,
362    pub(crate) proposal_ref: ProposalRef,
363    pub(crate) sender: Sender,
364}
365
366#[cfg(feature = "by_ref_proposal")]
367impl CachedProposal {
368    /// Deserialize the proposal
369    pub fn from_bytes(bytes: &[u8]) -> Result<Self, MlsError> {
370        Ok(Self::mls_decode(&mut &*bytes)?)
371    }
372
373    /// Serialize the proposal
374    pub fn to_bytes(&self) -> Result<Vec<u8>, MlsError> {
375        Ok(self.mls_encode_to_vec()?)
376    }
377
378    /// The proposal content.
379    pub fn proposal(&self) -> &Proposal {
380        &self.proposal
381    }
382
383    /// The proposal reference (hash-based identifier).
384    pub fn proposal_ref(&self) -> &ProposalRef {
385        &self.proposal_ref
386    }
387
388    /// The sender of the proposal.
389    pub fn sender(&self) -> &Sender {
390        &self.sender
391    }
392}
393
394#[cfg(feature = "by_ref_proposal")]
395impl ProposalMessageDescription {
396    pub fn cached_proposal(self) -> CachedProposal {
397        let sender = match self.sender {
398            ProposalSender::Member(i) => Sender::Member(i),
399            ProposalSender::External(i) => Sender::External(i),
400            ProposalSender::NewMember => Sender::NewMemberProposal,
401        };
402
403        CachedProposal {
404            proposal: self.proposal,
405            proposal_ref: self.proposal_ref,
406            sender,
407        }
408    }
409
410    pub fn proposal_ref(&self) -> Vec<u8> {
411        self.proposal_ref.to_vec()
412    }
413
414    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
415    pub(crate) async fn new<C: CipherSuiteProvider>(
416        cs: &C,
417        content: &AuthenticatedContent,
418        proposal: Proposal,
419    ) -> Result<Self, MlsError> {
420        Ok(ProposalMessageDescription {
421            authenticated_data: content.content.authenticated_data.clone(),
422            proposal,
423            sender: content.content.sender.try_into()?,
424            proposal_ref: ProposalRef::from_content(cs, content).await?,
425        })
426    }
427}
428
429#[cfg(not(feature = "by_ref_proposal"))]
430#[derive(Debug, Clone)]
431/// Description of a processed MLS proposal message.
432pub struct ProposalMessageDescription {}
433
434#[allow(clippy::large_enum_variant)]
435pub(crate) enum EventOrContent<E> {
436    #[cfg_attr(
437        not(all(feature = "private_message", feature = "external_client")),
438        allow(dead_code)
439    )]
440    Event(E),
441    Content(AuthenticatedContent),
442}
443
444#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
445#[cfg_attr(all(target_arch = "wasm32", mls_build_async), maybe_async::must_be_async(?Send))]
446#[cfg_attr(
447    all(not(target_arch = "wasm32"), mls_build_async),
448    maybe_async::must_be_async
449)]
450pub(crate) trait MessageProcessor: Send + Sync {
451    type OutputType: TryFrom<ApplicationMessageDescription, Error = MlsError>
452        + From<CommitMessageDescription>
453        + From<ProposalMessageDescription>
454        + From<GroupInfo>
455        + From<Welcome>
456        + From<KeyPackage>
457        + Send;
458
459    type MlsRules: MlsRules;
460    type IdentityProvider: IdentityProvider;
461    type CipherSuiteProvider: CipherSuiteProvider;
462    type PreSharedKeyStorage: PreSharedKeyStorage;
463
464    async fn process_incoming_message(
465        &mut self,
466        message: MlsMessage,
467        #[cfg(feature = "by_ref_proposal")] cache_proposal: bool,
468    ) -> Result<Self::OutputType, MlsError> {
469        self.process_incoming_message_with_time(
470            message,
471            #[cfg(feature = "by_ref_proposal")]
472            cache_proposal,
473            None,
474        )
475        .await
476    }
477
478    async fn process_incoming_message_with_time(
479        &mut self,
480        message: MlsMessage,
481        #[cfg(feature = "by_ref_proposal")] cache_proposal: bool,
482        time_sent: Option<MlsTime>,
483    ) -> Result<Self::OutputType, MlsError> {
484        #[cfg(all(feature = "export_key_generation", feature = "private_message"))]
485        // For encrypted application messages, retrieve the unauthenticated key
486        // generation used to decrypt the message and return it with the plaintext. Does
487        // not return an error on failure, allowing `get_event_from_incoming_message` to
488        // continue owning that task.
489        // Note that this decrypts the SenderData twice, which is not ideal.
490        let unauthn_key_gen_in_app_msg: Option<u32> = match message.payload {
491            MlsMessagePayload::Cipher(ref cipher_text) => self
492                .get_unauthenticated_key_generation_from_sender_data(cipher_text)
493                .unwrap_or_default(),
494            _ => None,
495        };
496
497        let event_or_content = self
498            .get_event_from_incoming_message(message, time_sent)
499            .await?;
500
501        self.process_event_or_content(
502            event_or_content,
503            #[cfg(feature = "by_ref_proposal")]
504            cache_proposal,
505            #[cfg(all(feature = "export_key_generation", feature = "private_message"))]
506            unauthn_key_gen_in_app_msg,
507            time_sent,
508        )
509        .await
510    }
511
512    async fn get_event_from_incoming_message(
513        &mut self,
514        message: MlsMessage,
515        time: Option<MlsTime>,
516    ) -> Result<EventOrContent<Self::OutputType>, MlsError> {
517        self.check_metadata(&message)?;
518
519        match message.payload {
520            MlsMessagePayload::Plain(plaintext) => {
521                self.verify_plaintext_authentication(plaintext).await
522            }
523            #[cfg(feature = "private_message")]
524            MlsMessagePayload::Cipher(cipher_text) => self.process_ciphertext(&cipher_text).await,
525            MlsMessagePayload::GroupInfo(group_info) => {
526                validate_group_info_member(
527                    self.group_state(),
528                    message.version,
529                    &group_info,
530                    self.cipher_suite_provider(),
531                )
532                .await?;
533
534                Ok(EventOrContent::Event(group_info.into()))
535            }
536            MlsMessagePayload::Welcome(welcome) => {
537                self.validate_welcome(&welcome, message.version)?;
538
539                Ok(EventOrContent::Event(welcome.into()))
540            }
541            MlsMessagePayload::KeyPackage(key_package) => {
542                self.validate_key_package(&key_package, message.version, time)
543                    .await?;
544
545                Ok(EventOrContent::Event(key_package.into()))
546            }
547        }
548    }
549
550    async fn process_event_or_content(
551        &mut self,
552        event_or_content: EventOrContent<Self::OutputType>,
553        #[cfg(feature = "by_ref_proposal")] cache_proposal: bool,
554        #[cfg(all(feature = "export_key_generation", feature = "private_message"))]
555        unauthn_key_gen_in_app_msg: Option<u32>,
556        time_sent: Option<MlsTime>,
557    ) -> Result<Self::OutputType, MlsError> {
558        let msg = match event_or_content {
559            EventOrContent::Event(event) => event,
560            EventOrContent::Content(content) => {
561                self.process_auth_content(
562                    content,
563                    #[cfg(feature = "by_ref_proposal")]
564                    cache_proposal,
565                    #[cfg(all(feature = "export_key_generation", feature = "private_message"))]
566                    unauthn_key_gen_in_app_msg,
567                    time_sent,
568                )
569                .await?
570            }
571        };
572
573        Ok(msg)
574    }
575
576    async fn process_auth_content(
577        &mut self,
578        auth_content: AuthenticatedContent,
579        #[cfg(feature = "by_ref_proposal")] cache_proposal: bool,
580        #[cfg(all(feature = "export_key_generation", feature = "private_message"))]
581        unauthn_key_gen_in_app_msg: Option<u32>,
582        time_sent: Option<MlsTime>,
583    ) -> Result<Self::OutputType, MlsError> {
584        let event = match auth_content.content.content {
585            #[cfg(feature = "private_message")]
586            Content::Application(data) => {
587                let authenticated_data = auth_content.content.authenticated_data;
588                let sender = auth_content.content.sender;
589
590                self.process_application_message(
591                    data,
592                    sender,
593                    authenticated_data,
594                    #[cfg(all(feature = "export_key_generation", feature = "private_message"))]
595                    unauthn_key_gen_in_app_msg,
596                )
597                .and_then(Self::OutputType::try_from)
598            }
599            Content::Commit(_) => self
600                .process_commit(auth_content, time_sent)
601                .await
602                .map(Self::OutputType::from),
603            #[cfg(feature = "by_ref_proposal")]
604            Content::Proposal(ref proposal) => self
605                .process_proposal(&auth_content, proposal, cache_proposal)
606                .await
607                .map(Self::OutputType::from),
608        }?;
609
610        Ok(event)
611    }
612
613    #[cfg(feature = "private_message")]
614    fn process_application_message(
615        &self,
616        data: ApplicationData,
617        sender: Sender,
618        authenticated_data: Vec<u8>,
619        #[cfg(all(feature = "export_key_generation", feature = "private_message"))]
620        unauthenticated_key_generation: Option<u32>,
621    ) -> Result<ApplicationMessageDescription, MlsError> {
622        let Sender::Member(sender_index) = sender else {
623            return Err(MlsError::InvalidSender);
624        };
625
626        Ok(ApplicationMessageDescription {
627            authenticated_data,
628            sender_index,
629            data,
630            #[cfg(all(feature = "export_key_generation", feature = "private_message"))]
631            unauthenticated_key_generation,
632        })
633    }
634
635    #[cfg(feature = "by_ref_proposal")]
636    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
637    async fn process_proposal(
638        &mut self,
639        auth_content: &AuthenticatedContent,
640        proposal: &Proposal,
641        cache_proposal: bool,
642    ) -> Result<ProposalMessageDescription, MlsError> {
643        let proposal = ProposalMessageDescription::new(
644            self.cipher_suite_provider(),
645            auth_content,
646            proposal.clone(),
647        )
648        .await?;
649
650        let group_state = self.group_state_mut();
651
652        if cache_proposal {
653            group_state.proposals.insert(
654                proposal.proposal_ref.clone(),
655                proposal.proposal.clone(),
656                auth_content.content.sender,
657            );
658        }
659
660        Ok(proposal)
661    }
662
663    async fn process_commit(
664        &mut self,
665        auth_content: AuthenticatedContent,
666        time_sent: Option<MlsTime>,
667    ) -> Result<CommitMessageDescription, MlsError> {
668        if self.group_state().pending_reinit.is_some() {
669            return Err(MlsError::GroupUsedAfterReInit);
670        }
671
672        // Update the new GroupContext's confirmed and interim transcript hashes using the new Commit.
673        let (interim_transcript_hash, confirmed_transcript_hash) = transcript_hashes(
674            self.cipher_suite_provider(),
675            &self.group_state().interim_transcript_hash,
676            &auth_content,
677        )
678        .await?;
679
680        #[cfg(any(feature = "private_message", feature = "by_ref_proposal"))]
681        let commit = match auth_content.content.content {
682            Content::Commit(commit) => Ok(commit),
683            _ => Err(MlsError::UnexpectedMessageType),
684        }?;
685
686        #[cfg(not(any(feature = "private_message", feature = "by_ref_proposal")))]
687        let Content::Commit(commit) = auth_content.content.content;
688
689        let group_state = self.group_state();
690        let id_provider = self.identity_provider();
691
692        #[cfg(feature = "by_ref_proposal")]
693        let proposals = group_state
694            .proposals
695            .resolve_for_commit(auth_content.content.sender, commit.proposals)?;
696
697        #[cfg(not(feature = "by_ref_proposal"))]
698        let proposals = resolve_for_commit(auth_content.content.sender, commit.proposals)?;
699
700        let mut provisional_state = group_state
701            .apply_resolved(
702                auth_content.content.sender,
703                proposals,
704                commit.path.as_ref().map(|path| &path.leaf_node),
705                &id_provider,
706                self.cipher_suite_provider(),
707                &self.psk_storage(),
708                &self.mls_rules(),
709                time_sent,
710                CommitDirection::Receive,
711            )
712            .await?;
713
714        let sender = commit_sender(&auth_content.content.sender, &provisional_state)?;
715
716        //Verify that the path value is populated if the proposals vector contains any Update
717        // or Remove proposals, or if it's empty. Otherwise, the path value MAY be omitted.
718        if path_update_required(&provisional_state.applied_proposals, &self.mls_rules())
719            && commit.path.is_none()
720        {
721            return Err(MlsError::CommitMissingPath);
722        }
723
724        let self_removed = self.removal_proposal(&provisional_state);
725        #[cfg(all(
726            feature = "by_ref_proposal",
727            feature = "custom_proposal",
728            feature = "self_remove_proposal"
729        ))]
730        let self_removed_by_self = self.self_removal_proposal(&provisional_state);
731
732        let is_self_removed = self_removed.is_some();
733        #[cfg(all(
734            feature = "by_ref_proposal",
735            feature = "custom_proposal",
736            feature = "self_remove_proposal"
737        ))]
738        let is_self_removed = is_self_removed || self_removed_by_self.is_some();
739
740        let update_path = match commit.path {
741            Some(update_path) => Some(
742                validate_update_path(
743                    &self.identity_provider(),
744                    self.cipher_suite_provider(),
745                    update_path,
746                    &provisional_state,
747                    sender,
748                    time_sent,
749                    &group_state.context,
750                )
751                .await?,
752            ),
753            None => None,
754        };
755
756        let commit_effect =
757            if let Some(reinit) = provisional_state.applied_proposals.reinitializations.pop() {
758                self.group_state_mut().pending_reinit = Some(reinit.proposal.clone());
759                CommitEffect::ReInit(reinit)
760            } else if let Some(remove_proposal) = self_removed {
761                let new_epoch = NewEpoch::new(self.group_state().clone(), &provisional_state);
762                CommitEffect::Removed {
763                    remover: remove_proposal.sender,
764                    new_epoch: Box::new(new_epoch),
765                }
766            } else {
767                CommitEffect::NewEpoch(Box::new(NewEpoch::new(
768                    self.group_state().clone(),
769                    &provisional_state,
770                )))
771            };
772
773        #[cfg(all(
774            feature = "by_ref_proposal",
775            feature = "custom_proposal",
776            feature = "self_remove_proposal"
777        ))]
778        let commit_effect = if let Some(self_remove_proposal) = self_removed_by_self {
779            let new_epoch = NewEpoch::new(self.group_state().clone(), &provisional_state);
780            CommitEffect::Removed {
781                remover: self_remove_proposal.sender,
782                new_epoch: Box::new(new_epoch),
783            }
784        } else {
785            commit_effect
786        };
787
788        let new_secrets = match update_path {
789            Some(update_path) if !is_self_removed => {
790                self.apply_update_path(sender, &update_path, &mut provisional_state)
791                    .await
792            }
793            _ => Ok(None),
794        }?;
795
796        // Update the transcript hash to get the new context.
797        provisional_state.group_context.confirmed_transcript_hash = confirmed_transcript_hash;
798
799        // Update the parent hashes in the new context
800        provisional_state
801            .public_tree
802            .update_hashes(&[sender], self.cipher_suite_provider())
803            .await?;
804
805        // Update the tree hash in the new context
806        provisional_state.group_context.tree_hash = provisional_state
807            .public_tree
808            .tree_hash(self.cipher_suite_provider())
809            .await?;
810
811        if let Some(confirmation_tag) = &auth_content.auth.confirmation_tag {
812            if !is_self_removed {
813                // Update the key schedule to calculate new private keys
814                self.update_key_schedule(
815                    new_secrets,
816                    interim_transcript_hash,
817                    confirmation_tag,
818                    provisional_state,
819                )
820                .await?;
821            }
822            Ok(CommitMessageDescription {
823                is_external: matches!(auth_content.content.sender, Sender::NewMemberCommit),
824                authenticated_data: auth_content.content.authenticated_data,
825                committer: *sender,
826                effect: commit_effect,
827            })
828        } else {
829            Err(MlsError::InvalidConfirmationTag)
830        }
831    }
832
833    fn group_state(&self) -> &GroupState;
834    fn group_state_mut(&mut self) -> &mut GroupState;
835    fn mls_rules(&self) -> Self::MlsRules;
836    fn identity_provider(&self) -> Self::IdentityProvider;
837    fn cipher_suite_provider(&self) -> &Self::CipherSuiteProvider;
838    fn psk_storage(&self) -> Self::PreSharedKeyStorage;
839
840    fn removal_proposal(
841        &self,
842        provisional_state: &ProvisionalState,
843    ) -> Option<ProposalInfo<RemoveProposal>>;
844
845    #[cfg(all(
846        feature = "by_ref_proposal",
847        feature = "custom_proposal",
848        feature = "self_remove_proposal"
849    ))]
850    fn self_removal_proposal(
851        &self,
852        provisional_state: &ProvisionalState,
853    ) -> Option<ProposalInfo<SelfRemoveProposal>>;
854
855    #[cfg(feature = "private_message")]
856    fn min_epoch_available(&self) -> Option<u64>;
857
858    fn check_metadata(&self, message: &MlsMessage) -> Result<(), MlsError> {
859        let context = &self.group_state().context;
860
861        if message.version != context.protocol_version {
862            return Err(MlsError::ProtocolVersionMismatch);
863        }
864
865        if let Some((group_id, epoch, content_type)) = match &message.payload {
866            MlsMessagePayload::Plain(plaintext) => Some((
867                &plaintext.content.group_id,
868                plaintext.content.epoch,
869                plaintext.content.content_type(),
870            )),
871            #[cfg(feature = "private_message")]
872            MlsMessagePayload::Cipher(ciphertext) => Some((
873                &ciphertext.group_id,
874                ciphertext.epoch,
875                ciphertext.content_type,
876            )),
877            _ => None,
878        } {
879            if group_id != &context.group_id {
880                return Err(MlsError::GroupIdMismatch);
881            }
882
883            match content_type {
884                ContentType::Commit => {
885                    if context.epoch != epoch {
886                        Err(MlsError::InvalidEpoch)
887                    } else {
888                        Ok(())
889                    }
890                }
891                #[cfg(feature = "by_ref_proposal")]
892                ContentType::Proposal => {
893                    if context.epoch != epoch {
894                        Err(MlsError::InvalidEpoch)
895                    } else {
896                        Ok(())
897                    }
898                }
899                #[cfg(feature = "private_message")]
900                ContentType::Application => {
901                    if let Some(min) = self.min_epoch_available() {
902                        if epoch < min {
903                            Err(MlsError::InvalidEpoch)
904                        } else {
905                            Ok(())
906                        }
907                    } else {
908                        Ok(())
909                    }
910                }
911            }?;
912
913            // Proposal and commit messages must be sent in the current epoch
914            let check_epoch = content_type == ContentType::Commit;
915
916            #[cfg(feature = "by_ref_proposal")]
917            let check_epoch = check_epoch || content_type == ContentType::Proposal;
918
919            if check_epoch && epoch != context.epoch {
920                return Err(MlsError::InvalidEpoch);
921            }
922
923            // Unencrypted application messages are not allowed
924            #[cfg(feature = "private_message")]
925            if !matches!(&message.payload, MlsMessagePayload::Cipher(_))
926                && content_type == ContentType::Application
927            {
928                return Err(MlsError::UnencryptedApplicationMessage);
929            }
930        }
931
932        Ok(())
933    }
934
935    fn validate_welcome(
936        &self,
937        welcome: &Welcome,
938        version: ProtocolVersion,
939    ) -> Result<(), MlsError> {
940        let state = self.group_state();
941
942        (welcome.cipher_suite == state.context.cipher_suite
943            && version == state.context.protocol_version)
944            .then_some(())
945            .ok_or(MlsError::InvalidWelcomeMessage)
946    }
947
948    async fn validate_key_package(
949        &self,
950        key_package: &KeyPackage,
951        version: ProtocolVersion,
952        time: Option<MlsTime>,
953    ) -> Result<(), MlsError> {
954        let cs = self.cipher_suite_provider();
955        let id = self.identity_provider();
956
957        validate_key_package(key_package, version, cs, &id, time).await
958    }
959
960    #[cfg(feature = "private_message")]
961    async fn process_ciphertext(
962        &mut self,
963        cipher_text: &PrivateMessage,
964    ) -> Result<EventOrContent<Self::OutputType>, MlsError>;
965
966    async fn verify_plaintext_authentication(
967        &self,
968        message: PublicMessage,
969    ) -> Result<EventOrContent<Self::OutputType>, MlsError>;
970
971    #[cfg(all(feature = "export_key_generation", feature = "private_message"))]
972    /// Returns the unauthenticated key generation used to decrypt the private message.
973    async fn get_unauthenticated_key_generation_from_sender_data(
974        &mut self,
975        cipher_text: &PrivateMessage,
976    ) -> Result<Option<u32>, MlsError>;
977
978    async fn apply_update_path(
979        &mut self,
980        sender: LeafIndex,
981        update_path: &ValidatedUpdatePath,
982        provisional_state: &mut ProvisionalState,
983    ) -> Result<Option<(TreeKemPrivate, PathSecret)>, MlsError> {
984        provisional_state
985            .public_tree
986            .apply_update_path(
987                sender,
988                update_path,
989                &provisional_state.group_context.extensions,
990                self.identity_provider(),
991                self.cipher_suite_provider(),
992            )
993            .await
994            .map(|_| None)
995    }
996
997    async fn update_key_schedule(
998        &mut self,
999        secrets: Option<(TreeKemPrivate, PathSecret)>,
1000        interim_transcript_hash: InterimTranscriptHash,
1001        confirmation_tag: &ConfirmationTag,
1002        provisional_public_state: ProvisionalState,
1003    ) -> Result<(), MlsError>;
1004}
1005
1006#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1007pub(crate) async fn validate_key_package<C: CipherSuiteProvider, I: IdentityProvider>(
1008    key_package: &KeyPackage,
1009    version: ProtocolVersion,
1010    cs: &C,
1011    id: &I,
1012    time: Option<MlsTime>,
1013) -> Result<(), MlsError> {
1014    let validator = LeafNodeValidator::new(cs, id, MemberValidationContext::None);
1015
1016    #[cfg(feature = "std")]
1017    let context = Some(MlsTime::now());
1018
1019    #[cfg(not(feature = "std"))]
1020    let context = None;
1021
1022    let context = if time.is_some() { time } else { context };
1023
1024    let context = ValidationContext::Add(context);
1025
1026    validator
1027        .check_if_valid(&key_package.leaf_node, context)
1028        .await?;
1029
1030    validate_key_package_properties(key_package, version, cs).await?;
1031
1032    Ok(())
1033}
1034
1035#[cfg(test)]
1036mod tests {
1037    use alloc::{vec, vec::Vec};
1038    use mls_rs_codec::{MlsDecode, MlsEncode};
1039
1040    use crate::{
1041        client::test_utils::TEST_PROTOCOL_VERSION,
1042        group::{test_utils::get_test_group_context, GroupState, Sender},
1043    };
1044
1045    use super::{CommitEffect, NewEpoch};
1046
1047    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1048    async fn commit_effect_codec() {
1049        let epoch = NewEpoch {
1050            epoch: 7,
1051            prior_state: GroupState {
1052                #[cfg(feature = "by_ref_proposal")]
1053                proposals: crate::group::ProposalCache::new(TEST_PROTOCOL_VERSION, vec![]),
1054                context: get_test_group_context(7, 7.into()).await,
1055                public_tree: Default::default(),
1056                interim_transcript_hash: vec![].into(),
1057                pending_reinit: None,
1058                confirmation_tag: Default::default(),
1059            },
1060            applied_proposals: vec![],
1061            unused_proposals: vec![],
1062        };
1063
1064        let effects = vec![
1065            CommitEffect::NewEpoch(epoch.clone().into()),
1066            CommitEffect::Removed {
1067                new_epoch: epoch.into(),
1068                remover: Sender::Member(0),
1069            },
1070        ];
1071
1072        let bytes = effects.mls_encode_to_vec().unwrap();
1073
1074        assert_eq!(
1075            effects,
1076            Vec::<CommitEffect>::mls_decode(&mut &*bytes).unwrap()
1077        );
1078    }
1079}