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