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