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 use openmls::prelude::SignaturePublicKey;
163
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 let id = conversation.id().clone();
177 assert!(alice.pending_proposals(&id).await.is_empty());
178
179 let proposal_guard = conversation.invite_proposal(&charlie).await;
180 assert_eq!(alice.pending_proposals(&id).await.len(), 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 let id = conversation.id().clone();
210
211 assert!(alice.pending_proposals(&id).await.is_empty());
212 let proposal_guard = conversation.remove_proposal(&charlie).await;
213 assert_eq!(alice.pending_proposals(&id).await.len(), 1);
214 let conversation = proposal_guard
215 .notify_members()
216 .await
217 .acting_as(&bob)
218 .await
219 .commit_pending_proposals_notify()
220 .await;
221 assert_eq!(conversation.member_count().await, 2);
222 assert!(conversation.is_functional_and_contains([&alice, &bob]).await)
223 })
224 .await
225 }
226 }
227
228 mod propose_self_update {
229 use super::*;
230
231 #[apply(all_cred_cipher)]
232 async fn can_propose_updating(case: TestContext) {
233 let [alice, bob] = case.sessions().await;
234 Box::pin(async move {
235 let conversation = case.create_conversation([&alice, &bob]).await;
236 let id = conversation.id().clone();
237
238 let bob_keys = bob
239 .get_conversation_unchecked(&id)
240 .await
241 .signature_keys()
242 .collect::<Vec<SignaturePublicKey>>();
243 let alice_keys = alice
244 .get_conversation_unchecked(&id)
245 .await
246 .signature_keys()
247 .collect::<Vec<SignaturePublicKey>>();
248 assert!(alice_keys.iter().all(|a_key| bob_keys.contains(a_key)));
249 let alice_key = alice.encryption_key_of(&id, alice.get_client_id().await).await;
250
251 let commit_guard = conversation
252 .update_proposal_notify()
253 .await
254 .acting_as(&bob)
255 .await
256 .commit_pending_proposals()
257 .await;
258
259 assert!(
260 !bob.get_conversation_unchecked(&id)
261 .await
262 .encryption_keys()
263 .contains(&alice_key)
264 );
265
266 assert!(
267 alice
268 .get_conversation_unchecked(&id)
269 .await
270 .encryption_keys()
271 .contains(&alice_key)
272 );
273 let conversation = commit_guard.notify_members().await;
276 assert!(
277 !alice
278 .get_conversation_unchecked(&id)
279 .await
280 .encryption_keys()
281 .contains(&alice_key)
282 );
283
284 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
286 })
287 .await;
288 }
289 }
290
291 mod delivery_semantics {
292 use super::*;
293
294 #[apply(all_cred_cipher)]
295 async fn should_prevent_out_of_order_proposals(case: TestContext) {
296 let [alice, bob] = case.sessions().await;
297 Box::pin(async move {
298 let conversation = case.create_conversation([&alice, &bob]).await;
299 let id = conversation.id().clone();
300
301 let proposal_guard = conversation.update_proposal().await;
302 let proposal = proposal_guard.message();
303 proposal_guard
304 .notify_members()
305 .await
306 .acting_as(&bob)
307 .await
308 .commit_pending_proposals_notify()
309 .await;
310 let past_proposal = bob
314 .transaction
315 .conversation(&id)
316 .await
317 .unwrap()
318 .decrypt_message(&proposal.to_bytes().unwrap())
319 .await;
320 assert!(matches!(past_proposal.unwrap_err(), Error::StaleProposal));
321 })
322 .await;
323 }
324 }
325}