mls_rs/group/
mls_rules.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
5use crate::group::{proposal_filter::ProposalBundle, Roster};
6
7#[cfg(feature = "private_message")]
8use crate::{
9    group::{padding::PaddingMode, Sender},
10    WireFormat,
11};
12
13use alloc::boxed::Box;
14use core::convert::Infallible;
15use mls_rs_core::{error::IntoAnyError, group::Member, identity::SigningIdentity};
16
17use super::GroupContext;
18
19#[derive(Copy, Clone, Debug, PartialEq, Eq)]
20pub enum CommitDirection {
21    Send,
22    Receive,
23}
24
25/// The source of the commit: either a current member or a new member joining
26/// via external commit.
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub enum CommitSource {
29    ExistingMember(Member),
30    NewMember(SigningIdentity),
31}
32
33/// Options controlling commit generation
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35#[non_exhaustive]
36pub struct CommitOptions {
37    pub path_required: bool,
38    pub ratchet_tree_extension: bool,
39    pub single_welcome_message: bool,
40    pub allow_external_commit: bool,
41    /// Include the ratchet tree out of band and not in the external group info, regardless
42    /// of the presence of the ratchet tree extension (doesn't affect commits/welcome).
43    pub always_out_of_band_ratchet_tree: bool,
44}
45
46impl Default for CommitOptions {
47    fn default() -> Self {
48        CommitOptions {
49            path_required: false,
50            ratchet_tree_extension: true,
51            single_welcome_message: true,
52            allow_external_commit: false,
53            always_out_of_band_ratchet_tree: false,
54        }
55    }
56}
57
58impl CommitOptions {
59    pub fn new() -> Self {
60        Self::default()
61    }
62
63    pub fn with_path_required(self, path_required: bool) -> Self {
64        Self {
65            path_required,
66            ..self
67        }
68    }
69
70    pub fn with_ratchet_tree_extension(self, ratchet_tree_extension: bool) -> Self {
71        Self {
72            ratchet_tree_extension,
73            ..self
74        }
75    }
76
77    pub fn with_single_welcome_message(self, single_welcome_message: bool) -> Self {
78        Self {
79            single_welcome_message,
80            ..self
81        }
82    }
83
84    pub fn with_allow_external_commit(self, allow_external_commit: bool) -> Self {
85        Self {
86            allow_external_commit,
87            ..self
88        }
89    }
90
91    pub fn with_always_out_of_band_ratchet_tree(
92        self,
93        always_out_of_band_ratchet_tree: bool,
94    ) -> Self {
95        Self {
96            always_out_of_band_ratchet_tree,
97            ..self
98        }
99    }
100}
101
102/// Options controlling encryption of control and application messages
103#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
104#[non_exhaustive]
105pub struct EncryptionOptions {
106    #[cfg(feature = "private_message")]
107    pub encrypt_control_messages: bool,
108    #[cfg(feature = "private_message")]
109    pub padding_mode: PaddingMode,
110}
111
112#[cfg(feature = "private_message")]
113impl EncryptionOptions {
114    pub fn new(encrypt_control_messages: bool, padding_mode: PaddingMode) -> Self {
115        Self {
116            encrypt_control_messages,
117            padding_mode,
118        }
119    }
120
121    pub(crate) fn control_wire_format(&self, sender: Sender) -> WireFormat {
122        match sender {
123            Sender::Member(_) if self.encrypt_control_messages => WireFormat::PrivateMessage,
124            _ => WireFormat::PublicMessage,
125        }
126    }
127}
128
129/// A set of user controlled rules that customize the behavior of MLS.
130#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
131#[cfg_attr(mls_build_async, maybe_async::must_be_async)]
132pub trait MlsRules: Send + Sync {
133    type Error: IntoAnyError;
134
135    /// This is called when preparing or receiving a commit to pre-process the set of committed
136    /// proposals.
137    ///
138    /// Both proposals received during the current epoch and at the time of commit
139    /// will be presented for validation and filtering. Filter and validate will
140    /// present a raw list of proposals. Standard MLS rules are applied internally
141    /// on the result of these rules.
142    ///
143    /// Each member of a group MUST apply the same proposal rules in order to
144    /// maintain a working group.
145    ///
146    /// Typically, any invalid proposal should result in an error. The exception are invalid
147    /// by-reference proposals processed when _preparing_ a commit, which should be filtered
148    /// out instead. This is to avoid the deadlock situation when no commit can be generated
149    /// after receiving an invalid set of proposal messages.
150    ///
151    /// `ProposalBundle` can be arbitrarily modified. For example, a Remove proposal that
152    /// removes a moderator can result in adding a GroupContextExtensions proposal that updates
153    /// the moderator list in the group context. The resulting `ProposalBundle` is validated
154    /// by the library.
155    async fn filter_proposals(
156        &self,
157        direction: CommitDirection,
158        source: CommitSource,
159        current_roster: &Roster,
160        current_context: &GroupContext,
161        proposals: ProposalBundle,
162    ) -> Result<ProposalBundle, Self::Error>;
163
164    /// This is called when preparing a commit to determine various options: whether to enforce an update
165    /// path in case it is not mandated by MLS, whether to include the ratchet tree in the welcome
166    /// message (if the commit adds members) and whether to generate a single welcome message, or one
167    /// welcome message for each added member.
168    ///
169    /// The `new_roster` and `new_extension_list` describe the group state after the commit.
170    fn commit_options(
171        &self,
172        new_roster: &Roster,
173        new_context: &GroupContext,
174        proposals: &ProposalBundle,
175    ) -> Result<CommitOptions, Self::Error>;
176
177    /// This is called when sending any packet. For proposals and commits, this determines whether to
178    /// encrypt them. For any encrypted packet, this determines the padding mode used.
179    ///
180    /// Note that for commits, the `current_roster` and `current_extension_list` describe the group state
181    /// before the commit, unlike in [commit_options](MlsRules::commit_options).
182    fn encryption_options(
183        &self,
184        current_roster: &Roster,
185        current_context: &GroupContext,
186    ) -> Result<EncryptionOptions, Self::Error>;
187}
188
189macro_rules! delegate_mls_rules {
190    ($implementer:ty) => {
191        #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
192        #[cfg_attr(mls_build_async, maybe_async::must_be_async)]
193        impl<T: MlsRules + ?Sized> MlsRules for $implementer {
194            type Error = T::Error;
195
196            #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
197            async fn filter_proposals(
198                &self,
199                direction: CommitDirection,
200                source: CommitSource,
201                current_roster: &Roster,
202                context: &GroupContext,
203                proposals: ProposalBundle,
204            ) -> Result<ProposalBundle, Self::Error> {
205                (**self)
206                    .filter_proposals(direction, source, current_roster, context, proposals)
207                    .await
208            }
209
210            fn commit_options(
211                &self,
212                roster: &Roster,
213                context: &GroupContext,
214                proposals: &ProposalBundle,
215            ) -> Result<CommitOptions, Self::Error> {
216                (**self).commit_options(roster, context, proposals)
217            }
218
219            fn encryption_options(
220                &self,
221                roster: &Roster,
222                context: &GroupContext,
223            ) -> Result<EncryptionOptions, Self::Error> {
224                (**self).encryption_options(roster, context)
225            }
226        }
227    };
228}
229
230delegate_mls_rules!(Box<T>);
231delegate_mls_rules!(&T);
232
233#[derive(Clone, Debug, Default)]
234#[non_exhaustive]
235/// Default MLS rules with pass-through proposal filter and customizable options.
236pub struct DefaultMlsRules {
237    pub commit_options: CommitOptions,
238    pub encryption_options: EncryptionOptions,
239}
240
241impl DefaultMlsRules {
242    /// Create new MLS rules with default settings: do not enforce path and do
243    /// put the ratchet tree in the extension.
244    pub fn new() -> Self {
245        Default::default()
246    }
247
248    /// Set commit options.
249    pub fn with_commit_options(self, commit_options: CommitOptions) -> Self {
250        Self {
251            commit_options,
252            encryption_options: self.encryption_options,
253        }
254    }
255
256    /// Set encryption options.
257    pub fn with_encryption_options(self, encryption_options: EncryptionOptions) -> Self {
258        Self {
259            commit_options: self.commit_options,
260            encryption_options,
261        }
262    }
263}
264
265#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
266#[cfg_attr(mls_build_async, maybe_async::must_be_async)]
267impl MlsRules for DefaultMlsRules {
268    type Error = Infallible;
269
270    async fn filter_proposals(
271        &self,
272        _direction: CommitDirection,
273        _source: CommitSource,
274        _current_roster: &Roster,
275        _: &GroupContext,
276        proposals: ProposalBundle,
277    ) -> Result<ProposalBundle, Self::Error> {
278        Ok(proposals)
279    }
280
281    fn commit_options(
282        &self,
283        _: &Roster,
284        _: &GroupContext,
285        _: &ProposalBundle,
286    ) -> Result<CommitOptions, Self::Error> {
287        Ok(self.commit_options)
288    }
289
290    fn encryption_options(
291        &self,
292        _: &Roster,
293        _: &GroupContext,
294    ) -> Result<EncryptionOptions, Self::Error> {
295        Ok(self.encryption_options)
296    }
297}