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