core_crypto/mls/conversation/
proposal.rs1use core_crypto_keystore::Database;
9use openmls::{binary_tree::LeafNodeIndex, framing::MlsMessageOut, key_packages::KeyPackageIn, prelude::LeafNode};
10
11use super::{Error, Result};
12use crate::{
13 MlsConversation, MlsError, MlsProposalRef, RecursiveError, Session,
14 e2e_identity::NewCrlDistributionPoints,
15 mls::credential::crl::{extract_crl_uris_from_credentials, get_new_crl_distribution_points},
16 mls_provider::MlsCryptoProvider,
17};
18
19impl MlsConversation {
21 #[cfg_attr(test, crate::durable)]
23 pub async fn propose_add_member(
24 &mut self,
25 client: &Session<Database>,
26 backend: &MlsCryptoProvider,
27 database: &Database,
28 key_package: KeyPackageIn,
29 ) -> Result<MlsProposalBundle> {
30 let signer = &self
31 .find_current_credential(client)
32 .await
33 .map_err(|_| Error::IdentityInitializationError)?
34 .signature_key_pair;
35
36 let crl_new_distribution_points = get_new_crl_distribution_points(
37 database,
38 extract_crl_uris_from_credentials(std::iter::once(key_package.credential().mls_credential()))
39 .map_err(RecursiveError::mls_credential("extracting crl uris from credentials"))?,
40 )
41 .await
42 .map_err(RecursiveError::mls_credential("getting new crl distribution points"))?;
43
44 let (proposal, proposal_ref) = self
45 .group
46 .propose_add_member(backend, signer, key_package)
47 .await
48 .map_err(MlsError::wrap("propose add member"))?;
49 let proposal = MlsProposalBundle {
50 proposal,
51 proposal_ref: proposal_ref.into(),
52 crl_new_distribution_points,
53 };
54 self.persist_group_when_changed(database, false).await?;
55 Ok(proposal)
56 }
57
58 #[cfg_attr(test, crate::durable)]
60 pub async fn propose_remove_member(
61 &mut self,
62 client: &Session<Database>,
63 provider: &MlsCryptoProvider,
64 database: &Database,
65 member: LeafNodeIndex,
66 ) -> Result<MlsProposalBundle> {
67 let signer = &self
68 .find_current_credential(client)
69 .await
70 .map_err(|_| Error::IdentityInitializationError)?
71 .signature_key_pair;
72 let proposal = self
73 .group
74 .propose_remove_member(provider, signer, member)
75 .map_err(MlsError::wrap("propose remove member"))
76 .map(MlsProposalBundle::from)?;
77 self.persist_group_when_changed(database, false).await?;
78 Ok(proposal)
79 }
80
81 #[cfg_attr(test, crate::durable)]
83 pub async fn propose_self_update(
84 &mut self,
85 client: &Session<Database>,
86 provider: &MlsCryptoProvider,
87 database: &Database,
88 ) -> Result<MlsProposalBundle> {
89 self.propose_explicit_self_update(client, provider, database, None)
90 .await
91 }
92
93 #[cfg_attr(test, crate::durable)]
95 pub async fn propose_explicit_self_update(
96 &mut self,
97 client: &Session<Database>,
98 backend: &MlsCryptoProvider,
99 database: &Database,
100 leaf_node: Option<LeafNode>,
101 ) -> Result<MlsProposalBundle> {
102 let msg_signer = &self
103 .find_current_credential(client)
104 .await
105 .map_err(|_| Error::IdentityInitializationError)?
106 .signature_key_pair;
107
108 let proposal = if let Some(own_leaf) = leaf_node {
109 let credential = self.find_credential_for_leaf_node(client, &own_leaf).await?;
110 self.group
111 .propose_explicit_self_update(backend, msg_signer, own_leaf, credential.signature_key())
112 .await
113 } else {
114 self.group.propose_self_update(backend, msg_signer).await
115 }
116 .map(MlsProposalBundle::from)
117 .map_err(MlsError::wrap("proposing explicit self update"))?;
118
119 self.persist_group_when_changed(database, false).await?;
120 Ok(proposal)
121 }
122}
123
124#[derive(Debug)]
126pub struct MlsProposalBundle {
127 pub proposal: MlsMessageOut,
129 pub proposal_ref: MlsProposalRef,
131 pub crl_new_distribution_points: NewCrlDistributionPoints,
133}
134
135impl From<(MlsMessageOut, openmls::prelude::hash_ref::ProposalRef)> for MlsProposalBundle {
136 fn from((proposal, proposal_ref): (MlsMessageOut, openmls::prelude::hash_ref::ProposalRef)) -> Self {
137 Self {
138 proposal,
139 proposal_ref: proposal_ref.into(),
140 crl_new_distribution_points: None.into(),
141 }
142 }
143}
144
145impl MlsProposalBundle {
146 #[allow(clippy::type_complexity)]
150 pub fn to_bytes(self) -> Result<(Vec<u8>, Vec<u8>, NewCrlDistributionPoints)> {
151 use openmls::prelude::TlsSerializeTrait as _;
152 let proposal = self
153 .proposal
154 .tls_serialize_detached()
155 .map_err(Error::tls_serialize("proposal"))?;
156 let proposal_ref = self.proposal_ref.to_bytes();
157
158 Ok((proposal, proposal_ref, self.crl_new_distribution_points))
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use itertools::Itertools;
165
166 use super::*;
167 use crate::{mls::conversation::ConversationWithMls as _, test_utils::*};
168
169 mod propose_add_members {
170 use super::*;
171
172 #[apply(all_cred_cipher)]
173 async fn can_propose_adding_members_to_conversation(case: TestContext) {
174 let [alice, bob, charlie] = case.sessions().await;
175 Box::pin(async move {
176 let conversation = case.create_conversation([&alice, &bob]).await;
177 assert!(!conversation.has_pending_proposals().await);
178
179 let proposal_guard = conversation.invite_proposal(&charlie).await;
180 assert_eq!(proposal_guard.conversation().pending_proposal_count().await, 1);
181 let commit_guard = proposal_guard
182 .notify_members()
183 .await
184 .acting_as(&bob)
185 .await
186 .commit_pending_proposals()
187 .await;
188 assert_eq!(commit_guard.conversation().members_counted_by(&bob).await, 3);
189 assert_eq!(commit_guard.conversation().members_counted_by(&alice).await, 2);
190
191 let conversation = commit_guard.notify_members().await;
194 assert_eq!(conversation.member_count().await, 3);
195 assert!(conversation.is_functional_and_contains([&alice, &bob, &charlie]).await)
196 })
197 .await
198 }
199 }
200
201 mod propose_remove_members {
202 use super::*;
203
204 #[apply(all_cred_cipher)]
205 async fn can_propose_removing_members_from_conversation(case: TestContext) {
206 let [alice, bob, charlie] = case.sessions().await;
207 Box::pin(async move {
208 let conversation = case.create_conversation([&alice, &bob, &charlie]).await;
209
210 assert!(!conversation.has_pending_proposals().await);
211 let proposal_guard = conversation.remove_proposal(&charlie).await;
212 assert_eq!(proposal_guard.conversation().pending_proposal_count().await, 1);
213 let conversation = proposal_guard
214 .notify_members()
215 .await
216 .acting_as(&bob)
217 .await
218 .commit_pending_proposals_notify()
219 .await;
220 assert_eq!(conversation.member_count().await, 2);
221 assert!(conversation.is_functional_and_contains([&alice, &bob]).await)
222 })
223 .await
224 }
225 }
226
227 mod propose_self_update {
228 use super::*;
229
230 #[apply(all_cred_cipher)]
231 async fn can_propose_updating(case: TestContext) {
232 let [alice, bob] = case.sessions().await;
233 Box::pin(async move {
234 let conversation = case.create_conversation([&alice, &bob]).await;
235
236 let bob_keys = conversation
237 .guard_of(&bob)
238 .await
239 .conversation()
240 .await
241 .signature_keys()
242 .collect::<Vec<_>>();
243 let alice_keys = conversation
244 .guard()
245 .await
246 .conversation()
247 .await
248 .signature_keys()
249 .collect::<Vec<_>>();
250 assert!(alice_keys.iter().all(|a_key| bob_keys.contains(a_key)));
251 let alice_key = conversation.encryption_public_key().await;
252
253 let commit_guard = conversation
254 .update_proposal_notify()
255 .await
256 .acting_as(&bob)
257 .await
258 .commit_pending_proposals()
259 .await;
260
261 let conversation = commit_guard.conversation();
262
263 assert!(
264 !conversation
265 .guard_of(&bob)
266 .await
267 .conversation()
268 .await
269 .encryption_keys()
270 .contains(&alice_key)
271 );
272
273 assert!(
274 conversation
275 .guard_of(&alice)
276 .await
277 .conversation()
278 .await
279 .encryption_keys()
280 .contains(&alice_key)
281 );
282 let conversation = commit_guard.notify_members().await;
285 assert!(
286 !conversation
287 .guard_of(&alice)
288 .await
289 .conversation()
290 .await
291 .encryption_keys()
292 .contains(&alice_key)
293 );
294
295 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
297 })
298 .await;
299 }
300 }
301
302 mod delivery_semantics {
303 use super::*;
304
305 #[apply(all_cred_cipher)]
306 async fn should_prevent_out_of_order_proposals(case: TestContext) {
307 let [alice, bob] = case.sessions().await;
308 Box::pin(async move {
309 let conversation = case.create_conversation([&alice, &bob]).await;
310 let id = conversation.id().clone();
311
312 let proposal_guard = conversation.update_proposal().await;
313 let proposal = proposal_guard.message();
314 proposal_guard
315 .notify_members()
316 .await
317 .acting_as(&bob)
318 .await
319 .commit_pending_proposals_notify()
320 .await;
321 let past_proposal = bob
325 .transaction
326 .conversation(&id)
327 .await
328 .unwrap()
329 .decrypt_message(&proposal.to_bytes().unwrap())
330 .await;
331 assert!(matches!(past_proposal.unwrap_err(), Error::StaleProposal));
332 })
333 .await;
334 }
335 }
336}