core_crypto/mls/conversation/
renew.rs1use core_crypto_keystore::entities::MlsEncryptionKeyPair;
2use openmls::prelude::{LeafNode, LeafNodeIndex, Proposal, QueuedProposal, Sender, StagedCommit};
3use openmls_traits::OpenMlsCryptoProvider;
4
5use mls_crypto_provider::MlsCryptoProvider;
6
7use super::{Error, Result};
8use crate::{
9 KeystoreError, RecursiveError,
10 prelude::{MlsConversation, MlsProposalBundle, Session},
11};
12
13pub(crate) struct Renew;
16
17impl Renew {
18 pub(crate) fn renew<'a>(
30 self_index: &LeafNodeIndex,
31 pending_proposals: impl Iterator<Item = QueuedProposal> + 'a,
32 pending_commit: Option<&'a StagedCommit>,
33 valid_commit: &'a StagedCommit,
34 ) -> (Vec<QueuedProposal>, bool) {
35 let mut needs_update = false;
39
40 let renewed_pending_proposals = if let Some(pending_commit) = pending_commit {
41 let commit_proposals = pending_commit.queued_proposals().cloned().collect::<Vec<_>>();
43
44 let empty_commit = commit_proposals.is_empty();
46
47 let valid_commit_has_own_update_proposal = valid_commit.update_proposals().any(|p| match p.sender() {
49 Sender::Member(sender_index) => self_index == sender_index,
50 _ => false,
51 });
52
53 needs_update = !valid_commit_has_own_update_proposal && empty_commit;
55
56 commit_proposals
58 .into_iter()
59 .filter_map(|p| Self::is_proposal_renewable(p, Some(valid_commit)))
60 .collect::<Vec<_>>()
61 } else {
62 pending_proposals
64 .filter_map(|p| Self::is_proposal_renewable(p, Some(valid_commit)))
65 .collect::<Vec<_>>()
66 };
67 (renewed_pending_proposals, needs_update)
68 }
69
70 fn is_proposal_renewable(proposal: QueuedProposal, commit: Option<&StagedCommit>) -> Option<QueuedProposal> {
72 if let Some(commit) = commit {
73 let in_commit = match proposal.proposal() {
74 Proposal::Add(add) => commit.add_proposals().any(|p| {
75 let commits_identity = p.add_proposal().key_package().leaf_node().credential().identity();
76 let proposal_identity = add.key_package().leaf_node().credential().identity();
77 commits_identity == proposal_identity
78 }),
79 Proposal::Remove(remove) => commit
80 .remove_proposals()
81 .any(|p| p.remove_proposal().removed() == remove.removed()),
82 Proposal::Update(update) => commit
83 .update_proposals()
84 .any(|p| p.update_proposal().leaf_node() == update.leaf_node()),
85 _ => true,
86 };
87 if in_commit { None } else { Some(proposal) }
88 } else {
89 Some(proposal)
91 }
92 }
93}
94
95impl MlsConversation {
96 pub(crate) async fn renew_proposals_for_current_epoch(
99 &mut self,
100 client: &Session,
101 backend: &MlsCryptoProvider,
102 proposals: impl Iterator<Item = QueuedProposal>,
103 needs_update: bool,
104 ) -> Result<Vec<MlsProposalBundle>> {
105 let mut bundle = vec![];
106 let is_external = |p: &QueuedProposal| matches!(p.sender(), Sender::External(_) | Sender::NewMemberProposal);
107 let proposals = proposals.filter(|p| !is_external(p));
108 for proposal in proposals {
109 let msg = match proposal.proposal {
110 Proposal::Add(add) => self.propose_add_member(client, backend, add.key_package.into()).await?,
111 Proposal::Remove(remove) => self.propose_remove_member(client, backend, remove.removed()).await?,
112 Proposal::Update(update) => self.renew_update(client, backend, Some(update.leaf_node())).await?,
113 _ => return Err(Error::ProposalVariantCannotBeRenewed),
114 };
115 bundle.push(msg);
116 }
117 if needs_update {
118 let proposal = self.renew_update(client, backend, None).await?;
119 bundle.push(proposal);
120 }
121 Ok(bundle)
122 }
123
124 async fn renew_update(
128 &mut self,
129 client: &Session,
130 backend: &MlsCryptoProvider,
131 leaf_node: Option<&LeafNode>,
132 ) -> Result<MlsProposalBundle> {
133 if let Some(leaf_node) = leaf_node {
134 backend
137 .key_store()
138 .remove::<MlsEncryptionKeyPair, _>(leaf_node.encryption_key().as_slice())
139 .await
140 .map_err(KeystoreError::wrap("removing mls encryption keypair"))?;
141 }
142
143 let mut leaf_node = leaf_node
144 .or_else(|| self.group.own_leaf())
145 .cloned()
146 .ok_or(Error::MlsGroupInvalidState("own_leaf is None"))?;
147
148 let sc = self.signature_scheme();
149 let ct = self.own_credential_type()?;
150 let cb = client
151 .find_most_recent_credential_bundle(sc, ct)
152 .await
153 .map_err(RecursiveError::mls_client("finding most recent credential bundle"))?;
154
155 leaf_node.set_credential_with_key(cb.to_mls_credential_with_key());
156
157 self.propose_explicit_self_update(client, backend, Some(leaf_node))
158 .await
159 }
160
161 pub(crate) fn self_pending_proposals(&self) -> impl Iterator<Item = &QueuedProposal> {
162 self.group
163 .pending_proposals()
164 .filter(|&p| matches!(p.sender(), Sender::Member(i) if i == &self.group.own_leaf_index()))
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use wasm_bindgen_test::*;
171
172 use crate::test_utils::*;
173
174 mod update {
175 use super::*;
176
177 #[apply(all_cred_cipher)]
178 #[wasm_bindgen_test]
179 pub async fn renewable_when_created_by_self(case: TestContext) {
180 let [alice, bob] = case.sessions().await;
181 Box::pin(async move {
182 let conversation = case.create_conversation([&alice, &bob]).await;
183 let id = conversation.id().clone();
184
185 assert!(alice.pending_proposals(&id).await.is_empty());
186 let propposal_guard = conversation.update_proposal().await;
187 assert_eq!(alice.pending_proposals(&id).await.len(), 1);
188
189 let (commit_guard, result) = propposal_guard
191 .finish()
192 .acting_as(&bob)
193 .await
194 .update()
195 .await
196 .notify_member_fallible(&alice)
197 .await;
198
199 let proposals = result.unwrap().proposals;
200 assert_eq!(alice.pending_proposals(&id).await.len(), 1);
202 assert_eq!(proposals.len(), alice.pending_proposals(&id).await.len());
203
204 let (_, result) = commit_guard
206 .finish()
207 .acting_as(&bob)
208 .await
209 .update()
210 .await
211 .notify_member_fallible(&alice)
212 .await;
213 let proposals = result.unwrap().proposals;
214 assert_eq!(alice.pending_proposals(&id).await.len(), 1);
217 assert_eq!(proposals.len(), alice.pending_proposals(&id).await.len());
218 })
219 .await
220 }
221
222 #[apply(all_cred_cipher)]
223 #[wasm_bindgen_test]
224 pub async fn not_renewable_when_in_valid_commit(case: TestContext) {
225 let [alice, bob] = case.sessions().await;
226 Box::pin(async move {
227 let conversation = case.create_conversation([&alice, &bob]).await;
228 let id = conversation.id().clone();
229
230 assert!(alice.pending_proposals(&id).await.is_empty());
231 let conversation = conversation.update_proposal_notify().await;
233 assert_eq!(alice.pending_proposals(&id).await.len(), 1);
234
235 let (commit_guard, result) = conversation
237 .acting_as(&bob)
238 .await
239 .update()
240 .await
241 .notify_member_fallible(&alice)
242 .await;
243
244 let proposals = result.unwrap().proposals;
245 assert!(alice.pending_proposals(&id).await.is_empty());
247 assert_eq!(proposals.len(), alice.pending_proposals(&id).await.len());
248
249 let (_, result) = commit_guard
250 .finish()
251 .update_proposal_notify()
252 .await
253 .acting_as(&bob)
254 .await
255 .update()
256 .await
257 .notify_member_fallible(&alice)
258 .await;
259 let proposals = result.unwrap().proposals;
260 assert!(alice.pending_proposals(&id).await.is_empty());
262 assert_eq!(proposals.len(), alice.pending_proposals(&id).await.len());
263 })
264 .await
265 }
266
267 #[apply(all_cred_cipher)]
268 #[wasm_bindgen_test]
269 pub async fn not_renewable_by_ref(case: TestContext) {
270 let [alice, bob, charlie] = case.sessions().await;
271 Box::pin(async move {
272 let conversation = case.create_conversation([&alice, &bob, &charlie]).await;
273 let id = conversation.id().clone();
274
275 let propposal_guard = conversation.acting_as(&bob).await.update_proposal().await;
276 assert!(alice.pending_proposals(&id).await.is_empty());
277 let propposal_guard = propposal_guard.notify_member(&alice).await;
278 assert_eq!(alice.pending_proposals(&id).await.len(), 1);
279
280 let (_, result) = propposal_guard
282 .finish()
283 .acting_as(&charlie)
284 .await
285 .update()
286 .await
287 .notify_member_fallible(&alice)
288 .await;
289 let proposals = result.unwrap().proposals;
290 assert!(alice.pending_proposals(&id).await.is_empty());
292 assert_eq!(proposals.len(), alice.pending_proposals(&id).await.len());
293 })
294 .await
295 }
296 }
297
298 mod add {
299 use super::*;
300
301 #[apply(all_cred_cipher)]
302 #[wasm_bindgen_test]
303 pub async fn not_renewable_when_valid_commit_adds_same(case: TestContext) {
304 let [alice, bob, charlie] = case.sessions().await;
305 Box::pin(async move {
306 let conversation = case.create_conversation([&alice, &bob]).await;
307 let id = conversation.id().clone();
308
309 assert!(alice.pending_proposals(&id).await.is_empty());
311 let proposal_guard = conversation.invite_proposal(&charlie).await;
312 assert_eq!(alice.pending_proposals(&id).await.len(), 1);
313
314 let (commit_guard, result) = proposal_guard
316 .finish()
317 .acting_as(&bob)
318 .await
319 .invite([&charlie])
320 .await
321 .notify_member_fallible(&alice)
322 .await;
323
324 let proposals = result.unwrap().proposals;
325 assert!(alice.pending_proposals(&id).await.is_empty());
327 assert_eq!(proposals.len(), alice.pending_proposals(&id).await.len());
328
329 let conversation = commit_guard.notify_members().await;
330 assert!(conversation.is_functional_and_contains([&alice, &bob, &charlie]).await);
331 })
332 .await
333 }
334
335 #[apply(all_cred_cipher)]
336 #[wasm_bindgen_test]
337 pub async fn not_renewable_in_pending_commit_when_valid_commit_adds_same(case: TestContext) {
338 let [alice, bob, charlie] = case.sessions().await;
339 Box::pin(async move {
340 let conversation = case.create_conversation([&alice, &bob]).await;
341 let id = conversation.id().clone();
342
343 assert!(alice.pending_proposals(&id).await.is_empty());
345 let proposal_guard = conversation.invite_proposal(&charlie).await;
346 assert_eq!(alice.pending_proposals(&id).await.len(), 1);
347
348 alice.commit_pending_proposals_unmerged(&id).await;
350 assert!(alice.pending_commit(&id).await.is_some());
351
352 let (commit_guard, result) = proposal_guard
354 .finish()
355 .acting_as(&bob)
356 .await
357 .invite([&charlie])
358 .await
359 .notify_member_fallible(&alice)
360 .await;
361
362 let proposals = result.unwrap().proposals;
363 assert!(alice.pending_proposals(&id).await.is_empty());
365 assert_eq!(proposals.len(), alice.pending_proposals(&id).await.len());
366
367 let conversation = commit_guard.notify_members().await;
368 assert!(conversation.is_functional_and_contains([&alice, &bob, &charlie]).await);
369 })
370 .await
371 }
372
373 #[apply(all_cred_cipher)]
374 #[wasm_bindgen_test]
375 pub async fn not_renewable_by_ref(case: TestContext) {
376 let [alice, bob, charlie, debbie] = case.sessions().await;
377 Box::pin(async move {
378 let conversation = case.create_conversation([&alice, &bob, &charlie]).await;
379 let id = conversation.id().clone();
380
381 let proposal_guard = conversation
383 .acting_as(&bob)
384 .await
385 .invite_proposal(&debbie)
386 .await
387 .notify_member(&alice)
388 .await;
389 assert_eq!(alice.pending_proposals(&id).await.len(), 1);
390
391 let (_, result) = proposal_guard
393 .finish()
394 .acting_as(&charlie)
395 .await
396 .update()
397 .await
398 .notify_member_fallible(&alice)
399 .await;
400
401 let proposals = result.unwrap().proposals;
402 assert!(alice.pending_proposals(&id).await.is_empty());
404 assert_eq!(proposals.len(), alice.pending_proposals(&id).await.len());
405 })
406 .await
407 }
408
409 #[apply(all_cred_cipher)]
410 #[wasm_bindgen_test]
411 pub async fn renewable_when_valid_commit_doesnt_adds_same(case: TestContext) {
412 let [alice, bob, charlie] = case.sessions().await;
413 Box::pin(async move {
414 let conversation = case.create_conversation([&alice, &bob]).await;
415 let id = conversation.id().clone();
416
417 assert!(alice.pending_proposals(&id).await.is_empty());
419 let propposal_guard = conversation.invite_proposal(&charlie).await;
420 assert_eq!(alice.pending_proposals(&id).await.len(), 1);
421
422 let (commit_guard, result) = propposal_guard
424 .finish()
425 .acting_as(&bob)
426 .await
427 .update()
428 .await
429 .notify_member_fallible(&alice)
430 .await;
431
432 let proposals = result.unwrap().proposals;
433 assert_eq!(alice.pending_proposals(&id).await.len(), 1);
435 assert_eq!(proposals.len(), alice.pending_proposals(&id).await.len());
436
437 alice.commit_pending_proposals_unmerged(&id).await;
439 assert!(alice.pending_commit(&id).await.is_some());
440 let (_, result) = commit_guard
441 .finish()
442 .acting_as(&bob)
443 .await
444 .update()
445 .await
446 .notify_member_fallible(&alice)
447 .await;
448
449 let proposals = result.unwrap().proposals;
450 assert_eq!(alice.pending_proposals(&id).await.len(), 1);
453 assert_eq!(proposals.len(), alice.pending_proposals(&id).await.len());
454 })
455 .await
456 }
457
458 #[apply(all_cred_cipher)]
459 #[wasm_bindgen_test]
460 pub async fn renews_pending_commit_when_valid_commit_doesnt_add_same(case: TestContext) {
461 let [alice, bob] = case.sessions().await;
462 Box::pin(async move {
463 let conversation = case.create_conversation([&alice, &bob]).await;
464 let id = conversation.id().clone();
465
466 alice.create_unmerged_commit(&id).await;
468 assert!(alice.pending_commit(&id).await.is_some());
469
470 let (_, result) = conversation
472 .acting_as(&bob)
473 .await
474 .update()
475 .await
476 .notify_member_fallible(&alice)
477 .await;
478
479 let proposals = result.unwrap().proposals;
480 assert_eq!(alice.pending_proposals(&id).await.len(), 1);
482 assert_eq!(proposals.len(), alice.pending_proposals(&id).await.len());
483 })
484 .await
485 }
486 }
487
488 mod remove {
489 use super::*;
490
491 #[apply(all_cred_cipher)]
492 #[wasm_bindgen_test]
493 pub async fn not_renewable_when_valid_commit_removes_same(case: TestContext) {
494 let [alice, bob, charlie] = case.sessions().await;
495 Box::pin(async move {
496 let conversation = case.create_conversation([&alice, &bob, &charlie]).await;
497 let id = conversation.id().clone();
498
499 assert!(alice.pending_proposals(&id).await.is_empty());
500 let proposal_guard = conversation.remove_proposal(&charlie).await;
501 assert_eq!(alice.pending_proposals(&id).await.len(), 1);
502
503 let (commit_guard, result) = proposal_guard
504 .finish()
505 .acting_as(&bob)
506 .await
507 .remove(&charlie)
508 .await
509 .notify_member_fallible(&alice)
510 .await;
511
512 let proposals = result.unwrap().proposals;
513 assert!(alice.pending_proposals(&id).await.is_empty());
515 assert_eq!(proposals.len(), alice.pending_proposals(&id).await.len());
516
517 let conversation = commit_guard.notify_members().await;
518 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
519 })
520 .await
521 }
522
523 #[apply(all_cred_cipher)]
524 #[wasm_bindgen_test]
525 pub async fn not_renewable_by_ref(case: TestContext) {
526 let [alice, bob, charlie] = case.sessions().await;
527 Box::pin(async move {
528 let conversation = case.create_conversation([&alice, &bob, &charlie]).await;
529 let id = conversation.id().clone();
530
531 assert!(alice.pending_proposals(&id).await.is_empty());
532 let proposal_guard = conversation
533 .acting_as(&bob)
534 .await
535 .remove_proposal(&charlie)
536 .await
537 .notify_member(&alice)
538 .await;
539 assert_eq!(alice.pending_proposals(&id).await.len(), 1);
540
541 let (_, result) = proposal_guard
542 .finish()
543 .acting_as(&charlie)
544 .await
545 .update()
546 .await
547 .notify_member_fallible(&alice)
548 .await;
549
550 let proposals = result.unwrap().proposals;
551 assert!(alice.pending_proposals(&id).await.is_empty());
553 assert_eq!(proposals.len(), alice.pending_proposals(&id).await.len());
554 })
555 .await
556 }
557
558 #[apply(all_cred_cipher)]
559 #[wasm_bindgen_test]
560 pub async fn renewable_when_valid_commit_doesnt_remove_same(case: TestContext) {
561 let [alice, bob, charlie, debbie] = case.sessions().await;
562 Box::pin(async move {
563 let conversation = case.create_conversation([&alice, &bob, &charlie, &debbie]).await;
564 let id = conversation.id().clone();
565
566 assert!(alice.pending_proposals(&id).await.is_empty());
568 let propposal_guard = conversation.remove_proposal(&charlie).await;
569 assert_eq!(alice.pending_proposals(&id).await.len(), 1);
570
571 let (_, result) = propposal_guard
573 .finish()
574 .acting_as(&bob)
575 .await
576 .remove(&debbie)
577 .await
578 .notify_member_fallible(&alice)
579 .await;
580
581 let proposals = result.unwrap().proposals;
582 assert_eq!(alice.pending_proposals(&id).await.len(), 1);
584 assert_eq!(proposals.len(), alice.pending_proposals(&id).await.len());
585 })
586 .await
587 }
588
589 #[apply(all_cred_cipher)]
590 #[wasm_bindgen_test]
591 pub async fn renews_pending_commit_when_commit_doesnt_remove_same(case: TestContext) {
592 let [alice, bob, charlie, debbie] = case.sessions().await;
593 Box::pin(async move {
594 let conversation = case.create_conversation([&alice, &bob, &charlie, &debbie]).await;
595 let id = conversation.id().clone();
596
597 assert!(alice.pending_proposals(&id).await.is_empty());
599 let propposal_guard = conversation.remove_proposal(&charlie).await;
600 assert_eq!(alice.pending_proposals(&id).await.len(), 1);
601 alice.commit_pending_proposals_unmerged(&id).await;
602 assert!(alice.pending_commit(&id).await.is_some());
603
604 let (_, result) = propposal_guard
606 .finish()
607 .acting_as(&bob)
608 .await
609 .remove(&debbie)
610 .await
611 .notify_member_fallible(&alice)
612 .await;
613 let proposals = result.unwrap().proposals;
614 assert_eq!(alice.pending_proposals(&id).await.len(), 1);
616 assert_eq!(proposals.len(), alice.pending_proposals(&id).await.len());
617 })
618 .await
619 }
620
621 #[apply(all_cred_cipher)]
622 #[wasm_bindgen_test]
623 pub async fn renews_pending_commit_from_proposal_when_commit_doesnt_remove_same(case: TestContext) {
624 let [alice, bob, charlie, debbie] = case.sessions().await;
625 Box::pin(async move {
626 let conversation = case.create_conversation([&alice, &bob, &charlie, &debbie]).await;
627 let id = conversation.id().clone();
628
629 let propposal_guard = conversation.remove_proposal(&charlie).await;
631 alice.commit_pending_proposals_unmerged(&id).await;
632
633 let (_, result) = propposal_guard
635 .finish()
636 .acting_as(&bob)
637 .await
638 .remove(&debbie)
639 .await
640 .notify_member_fallible(&alice)
641 .await;
642 let proposals = result.unwrap().proposals;
643 assert_eq!(alice.pending_proposals(&id).await.len(), 1);
645 assert_eq!(proposals.len(), alice.pending_proposals(&id).await.len());
646 })
647 .await
648 }
649 }
650}