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