1 // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 // Copyright by contributors to this project.
3 // SPDX-License-Identifier: (Apache-2.0 OR MIT)
4
5 use alloc::vec::Vec;
6
7 use super::{
8 message_processor::ProvisionalState,
9 mls_rules::{CommitDirection, CommitSource, MlsRules},
10 GroupState, ProposalOrRef,
11 };
12 use crate::{
13 client::MlsError,
14 group::{
15 proposal_filter::{ProposalApplier, ProposalBundle, ProposalSource},
16 Proposal, Sender,
17 },
18 time::MlsTime,
19 };
20
21 #[cfg(feature = "by_ref_proposal")]
22 use crate::{
23 group::{
24 message_hash::MessageHash, proposal_filter::FilterStrategy, ProposalMessageDescription,
25 ProposalRef, ProtocolVersion,
26 },
27 MlsMessage,
28 };
29
30 use crate::tree_kem::leaf_node::LeafNode;
31
32 #[cfg(feature = "by_ref_proposal")]
33 use mls_rs_codec::{MlsDecode, MlsEncode, MlsSize};
34
35 use mls_rs_core::{
36 crypto::CipherSuiteProvider, error::IntoAnyError, identity::IdentityProvider,
37 psk::PreSharedKeyStorage,
38 };
39
40 #[cfg(feature = "by_ref_proposal")]
41 use core::fmt::{self, Debug};
42
43 #[cfg(feature = "by_ref_proposal")]
44 #[derive(Debug, Clone, MlsSize, MlsEncode, MlsDecode, PartialEq)]
45 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
46 pub struct CachedProposal {
47 pub(crate) proposal: Proposal,
48 pub(crate) sender: Sender,
49 }
50
51 #[cfg(feature = "by_ref_proposal")]
52 #[derive(Clone)]
53 pub(crate) struct ProposalCache {
54 protocol_version: ProtocolVersion,
55 group_id: Vec<u8>,
56 pub(crate) proposals: crate::map::SmallMap<ProposalRef, CachedProposal>,
57 pub(crate) own_proposals: crate::map::SmallMap<MessageHash, ProposalMessageDescription>,
58 }
59
60 #[cfg(feature = "by_ref_proposal")]
61 impl PartialEq for ProposalCache {
eq(&self, other: &Self) -> bool62 fn eq(&self, other: &Self) -> bool {
63 self.protocol_version == other.protocol_version
64 && self.group_id == other.group_id
65 && self.proposals == other.proposals
66 }
67 }
68
69 #[cfg(feature = "by_ref_proposal")]
70 impl Debug for ProposalCache {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 f.debug_struct("ProposalCache")
73 .field("protocol_version", &self.protocol_version)
74 .field(
75 "group_id",
76 &mls_rs_core::debug::pretty_group_id(&self.group_id),
77 )
78 .field("proposals", &self.proposals)
79 .finish()
80 }
81 }
82
83 #[cfg(feature = "by_ref_proposal")]
84 impl ProposalCache {
new(protocol_version: ProtocolVersion, group_id: Vec<u8>) -> Self85 pub fn new(protocol_version: ProtocolVersion, group_id: Vec<u8>) -> Self {
86 Self {
87 protocol_version,
88 group_id,
89 proposals: Default::default(),
90 own_proposals: Default::default(),
91 }
92 }
93
import( protocol_version: ProtocolVersion, group_id: Vec<u8>, proposals: crate::map::SmallMap<ProposalRef, CachedProposal>, own_proposals: crate::map::SmallMap<MessageHash, ProposalMessageDescription>, ) -> Self94 pub fn import(
95 protocol_version: ProtocolVersion,
96 group_id: Vec<u8>,
97 proposals: crate::map::SmallMap<ProposalRef, CachedProposal>,
98 own_proposals: crate::map::SmallMap<MessageHash, ProposalMessageDescription>,
99 ) -> Self {
100 Self {
101 protocol_version,
102 group_id,
103 proposals,
104 own_proposals,
105 }
106 }
107
clear(&mut self)108 pub fn clear(&mut self) {
109 self.proposals.clear();
110 self.own_proposals.clear();
111 }
112
113 #[cfg(feature = "private_message")]
114 #[inline]
is_empty(&self) -> bool115 pub fn is_empty(&self) -> bool {
116 self.proposals.is_empty()
117 }
118
insert(&mut self, proposal_ref: ProposalRef, proposal: Proposal, sender: Sender)119 pub fn insert(&mut self, proposal_ref: ProposalRef, proposal: Proposal, sender: Sender) {
120 let cached_proposal = CachedProposal { proposal, sender };
121
122 #[cfg(feature = "std")]
123 self.proposals.insert(proposal_ref, cached_proposal);
124
125 #[cfg(not(feature = "std"))]
126 // This may result in dups but it does not matter
127 self.proposals.push((proposal_ref, cached_proposal));
128 }
129
130 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
insert_own<CS: CipherSuiteProvider>( &mut self, proposal: ProposalMessageDescription, message: &MlsMessage, sender: Sender, cs: &CS, ) -> Result<(), MlsError>131 pub async fn insert_own<CS: CipherSuiteProvider>(
132 &mut self,
133 proposal: ProposalMessageDescription,
134 message: &MlsMessage,
135 sender: Sender,
136 cs: &CS,
137 ) -> Result<(), MlsError> {
138 self.insert(
139 proposal.proposal_ref.clone(),
140 proposal.proposal.clone(),
141 sender,
142 );
143
144 let message_hash = MessageHash::compute(cs, message).await?;
145 self.own_proposals.insert(message_hash, proposal);
146
147 Ok(())
148 }
149
prepare_commit( &self, sender: Sender, additional_proposals: Vec<Proposal>, ) -> ProposalBundle150 pub fn prepare_commit(
151 &self,
152 sender: Sender,
153 additional_proposals: Vec<Proposal>,
154 ) -> ProposalBundle {
155 self.proposals
156 .iter()
157 .map(|(r, p)| {
158 (
159 p.proposal.clone(),
160 p.sender,
161 ProposalSource::ByReference(r.clone()),
162 )
163 })
164 .chain(
165 additional_proposals
166 .into_iter()
167 .map(|p| (p, sender, ProposalSource::ByValue)),
168 )
169 .collect()
170 }
171
resolve_for_commit( &self, sender: Sender, proposal_list: Vec<ProposalOrRef>, ) -> Result<ProposalBundle, MlsError>172 pub fn resolve_for_commit(
173 &self,
174 sender: Sender,
175 proposal_list: Vec<ProposalOrRef>,
176 ) -> Result<ProposalBundle, MlsError> {
177 let mut proposals = ProposalBundle::default();
178
179 for p in proposal_list {
180 match p {
181 ProposalOrRef::Proposal(p) => proposals.add(*p, sender, ProposalSource::ByValue),
182 ProposalOrRef::Reference(r) => {
183 #[cfg(feature = "std")]
184 let p = self
185 .proposals
186 .get(&r)
187 .ok_or(MlsError::ProposalNotFound)?
188 .clone();
189 #[cfg(not(feature = "std"))]
190 let p = self
191 .proposals
192 .iter()
193 .find_map(|(rr, p)| (rr == &r).then_some(p))
194 .ok_or(MlsError::ProposalNotFound)?
195 .clone();
196
197 proposals.add(p.proposal, p.sender, ProposalSource::ByReference(r));
198 }
199 };
200 }
201
202 Ok(proposals)
203 }
204
205 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
get_own<CS: CipherSuiteProvider>( &self, cs: &CS, message: &MlsMessage, ) -> Result<Option<ProposalMessageDescription>, MlsError>206 pub async fn get_own<CS: CipherSuiteProvider>(
207 &self,
208 cs: &CS,
209 message: &MlsMessage,
210 ) -> Result<Option<ProposalMessageDescription>, MlsError> {
211 let message_hash = MessageHash::compute(cs, message).await?;
212
213 Ok(self.own_proposals.get(&message_hash).cloned())
214 }
215 }
216
217 #[cfg(not(feature = "by_ref_proposal"))]
prepare_commit( sender: Sender, additional_proposals: Vec<Proposal>, ) -> ProposalBundle218 pub(crate) fn prepare_commit(
219 sender: Sender,
220 additional_proposals: Vec<Proposal>,
221 ) -> ProposalBundle {
222 let mut proposals = ProposalBundle::default();
223
224 for p in additional_proposals.into_iter() {
225 proposals.add(p, sender, ProposalSource::ByValue);
226 }
227
228 proposals
229 }
230
231 #[cfg(not(feature = "by_ref_proposal"))]
resolve_for_commit( sender: Sender, proposal_list: Vec<ProposalOrRef>, ) -> Result<ProposalBundle, MlsError>232 pub(crate) fn resolve_for_commit(
233 sender: Sender,
234 proposal_list: Vec<ProposalOrRef>,
235 ) -> Result<ProposalBundle, MlsError> {
236 let mut proposals = ProposalBundle::default();
237
238 for p in proposal_list {
239 let ProposalOrRef::Proposal(p) = p;
240 proposals.add(*p, sender, ProposalSource::ByValue);
241 }
242
243 Ok(proposals)
244 }
245
246 impl GroupState {
247 #[inline(never)]
248 #[allow(clippy::too_many_arguments)]
249 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
apply_resolved<C, F, P, CSP>( &self, sender: Sender, mut proposals: ProposalBundle, external_leaf: Option<&LeafNode>, identity_provider: &C, cipher_suite_provider: &CSP, psk_storage: &P, user_rules: &F, commit_time: Option<MlsTime>, direction: CommitDirection, ) -> Result<ProvisionalState, MlsError> where C: IdentityProvider, F: MlsRules, P: PreSharedKeyStorage, CSP: CipherSuiteProvider,250 pub(crate) async fn apply_resolved<C, F, P, CSP>(
251 &self,
252 sender: Sender,
253 mut proposals: ProposalBundle,
254 external_leaf: Option<&LeafNode>,
255 identity_provider: &C,
256 cipher_suite_provider: &CSP,
257 psk_storage: &P,
258 user_rules: &F,
259 commit_time: Option<MlsTime>,
260 direction: CommitDirection,
261 ) -> Result<ProvisionalState, MlsError>
262 where
263 C: IdentityProvider,
264 F: MlsRules,
265 P: PreSharedKeyStorage,
266 CSP: CipherSuiteProvider,
267 {
268 let roster = self.public_tree.roster();
269 let group_extensions = &self.context.extensions;
270
271 #[cfg(feature = "by_ref_proposal")]
272 let all_proposals = proposals.clone();
273
274 let origin = match sender {
275 Sender::Member(index) => Ok::<_, MlsError>(CommitSource::ExistingMember(
276 roster.member_with_index(index)?,
277 )),
278 #[cfg(feature = "by_ref_proposal")]
279 Sender::NewMemberProposal => Err(MlsError::InvalidSender),
280 #[cfg(feature = "by_ref_proposal")]
281 Sender::External(_) => Err(MlsError::InvalidSender),
282 Sender::NewMemberCommit => Ok(CommitSource::NewMember(
283 external_leaf
284 .map(|l| l.signing_identity.clone())
285 .ok_or(MlsError::ExternalCommitMustHaveNewLeaf)?,
286 )),
287 }?;
288
289 proposals = user_rules
290 .filter_proposals(direction, origin, &roster, group_extensions, proposals)
291 .await
292 .map_err(|e| MlsError::MlsRulesError(e.into_any_error()))?;
293
294 let applier = ProposalApplier::new(
295 &self.public_tree,
296 self.context.protocol_version,
297 cipher_suite_provider,
298 group_extensions,
299 external_leaf,
300 identity_provider,
301 psk_storage,
302 #[cfg(feature = "by_ref_proposal")]
303 &self.context.group_id,
304 );
305
306 #[cfg(feature = "by_ref_proposal")]
307 let applier_output = match direction {
308 CommitDirection::Send => {
309 applier
310 .apply_proposals(FilterStrategy::IgnoreByRef, &sender, proposals, commit_time)
311 .await?
312 }
313 CommitDirection::Receive => {
314 applier
315 .apply_proposals(FilterStrategy::IgnoreNone, &sender, proposals, commit_time)
316 .await?
317 }
318 };
319
320 #[cfg(not(feature = "by_ref_proposal"))]
321 let applier_output = applier
322 .apply_proposals(&sender, &proposals, commit_time)
323 .await?;
324
325 #[cfg(feature = "by_ref_proposal")]
326 let unused_proposals = unused_proposals(
327 match direction {
328 CommitDirection::Send => all_proposals,
329 CommitDirection::Receive => self.proposals.proposals.iter().collect(),
330 },
331 &applier_output.applied_proposals,
332 );
333
334 let mut group_context = self.context.clone();
335 group_context.epoch += 1;
336
337 if let Some(ext) = applier_output.new_context_extensions {
338 group_context.extensions = ext;
339 }
340
341 #[cfg(feature = "by_ref_proposal")]
342 let proposals = applier_output.applied_proposals;
343
344 Ok(ProvisionalState {
345 public_tree: applier_output.new_tree,
346 group_context,
347 applied_proposals: proposals,
348 external_init_index: applier_output.external_init_index,
349 indexes_of_added_kpkgs: applier_output.indexes_of_added_kpkgs,
350 #[cfg(feature = "by_ref_proposal")]
351 unused_proposals,
352 })
353 }
354 }
355
356 #[cfg(feature = "by_ref_proposal")]
357 impl Extend<(ProposalRef, CachedProposal)> for ProposalCache {
extend<T>(&mut self, iter: T) where T: IntoIterator<Item = (ProposalRef, CachedProposal)>,358 fn extend<T>(&mut self, iter: T)
359 where
360 T: IntoIterator<Item = (ProposalRef, CachedProposal)>,
361 {
362 self.proposals.extend(iter);
363 }
364 }
365
366 #[cfg(feature = "by_ref_proposal")]
has_ref(proposals: &ProposalBundle, reference: &ProposalRef) -> bool367 fn has_ref(proposals: &ProposalBundle, reference: &ProposalRef) -> bool {
368 proposals
369 .iter_proposals()
370 .any(|p| matches!(&p.source, ProposalSource::ByReference(r) if r == reference))
371 }
372
373 #[cfg(feature = "by_ref_proposal")]
unused_proposals( all_proposals: ProposalBundle, accepted_proposals: &ProposalBundle, ) -> Vec<crate::mls_rules::ProposalInfo<Proposal>>374 fn unused_proposals(
375 all_proposals: ProposalBundle,
376 accepted_proposals: &ProposalBundle,
377 ) -> Vec<crate::mls_rules::ProposalInfo<Proposal>> {
378 all_proposals
379 .into_proposals()
380 .filter(|p| {
381 matches!(p.source, ProposalSource::ByReference(ref r) if !has_ref(accepted_proposals, r)
382 )
383 })
384 .collect()
385 }
386
387 // TODO add tests for lite version of filtering
388 #[cfg(all(feature = "by_ref_proposal", test))]
389 pub(crate) mod test_utils {
390 use mls_rs_core::{
391 crypto::CipherSuiteProvider, extension::ExtensionList, identity::IdentityProvider,
392 psk::PreSharedKeyStorage,
393 };
394
395 use crate::{
396 client::test_utils::TEST_PROTOCOL_VERSION,
397 group::{
398 confirmation_tag::ConfirmationTag,
399 mls_rules::{CommitDirection, DefaultMlsRules, MlsRules},
400 proposal::{Proposal, ProposalOrRef},
401 proposal_ref::ProposalRef,
402 state::GroupState,
403 test_utils::{get_test_group_context, TEST_GROUP},
404 GroupContext, LeafIndex, LeafNode, ProvisionalState, Sender, TreeKemPublic,
405 },
406 identity::{basic::BasicIdentityProvider, test_utils::BasicWithCustomProvider},
407 psk::AlwaysFoundPskStorage,
408 };
409
410 use super::{CachedProposal, MlsError, ProposalCache};
411
412 use alloc::vec::Vec;
413
414 impl CachedProposal {
new(proposal: Proposal, sender: Sender) -> Self415 pub fn new(proposal: Proposal, sender: Sender) -> Self {
416 Self { proposal, sender }
417 }
418 }
419
420 #[derive(Debug)]
421 pub(crate) struct CommitReceiver<'a, C, F, P, CSP> {
422 tree: &'a TreeKemPublic,
423 sender: Sender,
424 receiver: LeafIndex,
425 cache: ProposalCache,
426 identity_provider: C,
427 cipher_suite_provider: CSP,
428 group_context_extensions: ExtensionList,
429 user_rules: F,
430 with_psk_storage: P,
431 }
432
433 impl<'a, CSP>
434 CommitReceiver<'a, BasicWithCustomProvider, DefaultMlsRules, AlwaysFoundPskStorage, CSP>
435 {
new<S>( tree: &'a TreeKemPublic, sender: S, receiver: LeafIndex, cipher_suite_provider: CSP, ) -> Self where S: Into<Sender>,436 pub fn new<S>(
437 tree: &'a TreeKemPublic,
438 sender: S,
439 receiver: LeafIndex,
440 cipher_suite_provider: CSP,
441 ) -> Self
442 where
443 S: Into<Sender>,
444 {
445 Self {
446 tree,
447 sender: sender.into(),
448 receiver,
449 cache: make_proposal_cache(),
450 identity_provider: BasicWithCustomProvider::new(BasicIdentityProvider),
451 group_context_extensions: Default::default(),
452 user_rules: pass_through_rules(),
453 with_psk_storage: AlwaysFoundPskStorage,
454 cipher_suite_provider,
455 }
456 }
457 }
458
459 impl<'a, C, F, P, CSP> CommitReceiver<'a, C, F, P, CSP>
460 where
461 C: IdentityProvider,
462 F: MlsRules,
463 P: PreSharedKeyStorage,
464 CSP: CipherSuiteProvider,
465 {
466 #[cfg(feature = "by_ref_proposal")]
with_identity_provider<V>(self, validator: V) -> CommitReceiver<'a, V, F, P, CSP> where V: IdentityProvider,467 pub fn with_identity_provider<V>(self, validator: V) -> CommitReceiver<'a, V, F, P, CSP>
468 where
469 V: IdentityProvider,
470 {
471 CommitReceiver {
472 tree: self.tree,
473 sender: self.sender,
474 receiver: self.receiver,
475 cache: self.cache,
476 identity_provider: validator,
477 group_context_extensions: self.group_context_extensions,
478 user_rules: self.user_rules,
479 with_psk_storage: self.with_psk_storage,
480 cipher_suite_provider: self.cipher_suite_provider,
481 }
482 }
483
with_user_rules<G>(self, f: G) -> CommitReceiver<'a, C, G, P, CSP> where G: MlsRules,484 pub fn with_user_rules<G>(self, f: G) -> CommitReceiver<'a, C, G, P, CSP>
485 where
486 G: MlsRules,
487 {
488 CommitReceiver {
489 tree: self.tree,
490 sender: self.sender,
491 receiver: self.receiver,
492 cache: self.cache,
493 identity_provider: self.identity_provider,
494 group_context_extensions: self.group_context_extensions,
495 user_rules: f,
496 with_psk_storage: self.with_psk_storage,
497 cipher_suite_provider: self.cipher_suite_provider,
498 }
499 }
500
with_psk_storage<V>(self, v: V) -> CommitReceiver<'a, C, F, V, CSP> where V: PreSharedKeyStorage,501 pub fn with_psk_storage<V>(self, v: V) -> CommitReceiver<'a, C, F, V, CSP>
502 where
503 V: PreSharedKeyStorage,
504 {
505 CommitReceiver {
506 tree: self.tree,
507 sender: self.sender,
508 receiver: self.receiver,
509 cache: self.cache,
510 identity_provider: self.identity_provider,
511 group_context_extensions: self.group_context_extensions,
512 user_rules: self.user_rules,
513 with_psk_storage: v,
514 cipher_suite_provider: self.cipher_suite_provider,
515 }
516 }
517
518 #[cfg(feature = "by_ref_proposal")]
with_extensions(self, extensions: ExtensionList) -> Self519 pub fn with_extensions(self, extensions: ExtensionList) -> Self {
520 Self {
521 group_context_extensions: extensions,
522 ..self
523 }
524 }
525
cache<S>(mut self, r: ProposalRef, p: Proposal, proposer: S) -> Self where S: Into<Sender>,526 pub fn cache<S>(mut self, r: ProposalRef, p: Proposal, proposer: S) -> Self
527 where
528 S: Into<Sender>,
529 {
530 self.cache.insert(r, p, proposer.into());
531 self
532 }
533
534 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
receive<I>(&self, proposals: I) -> Result<ProvisionalState, MlsError> where I: IntoIterator, I::Item: Into<ProposalOrRef>,535 pub async fn receive<I>(&self, proposals: I) -> Result<ProvisionalState, MlsError>
536 where
537 I: IntoIterator,
538 I::Item: Into<ProposalOrRef>,
539 {
540 self.cache
541 .resolve_for_commit_default(
542 self.sender,
543 proposals.into_iter().map(Into::into).collect(),
544 None,
545 &self.group_context_extensions,
546 &self.identity_provider,
547 &self.cipher_suite_provider,
548 self.tree,
549 &self.with_psk_storage,
550 &self.user_rules,
551 )
552 .await
553 }
554 }
555
make_proposal_cache() -> ProposalCache556 pub(crate) fn make_proposal_cache() -> ProposalCache {
557 ProposalCache::new(TEST_PROTOCOL_VERSION, TEST_GROUP.to_vec())
558 }
559
pass_through_rules() -> DefaultMlsRules560 pub fn pass_through_rules() -> DefaultMlsRules {
561 DefaultMlsRules::new()
562 }
563
564 impl ProposalCache {
565 #[allow(clippy::too_many_arguments)]
566 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
resolve_for_commit_default<C, F, P, CSP>( &self, sender: Sender, proposal_list: Vec<ProposalOrRef>, external_leaf: Option<&LeafNode>, group_extensions: &ExtensionList, identity_provider: &C, cipher_suite_provider: &CSP, public_tree: &TreeKemPublic, psk_storage: &P, user_rules: F, ) -> Result<ProvisionalState, MlsError> where C: IdentityProvider, F: MlsRules, P: PreSharedKeyStorage, CSP: CipherSuiteProvider,567 pub async fn resolve_for_commit_default<C, F, P, CSP>(
568 &self,
569 sender: Sender,
570 proposal_list: Vec<ProposalOrRef>,
571 external_leaf: Option<&LeafNode>,
572 group_extensions: &ExtensionList,
573 identity_provider: &C,
574 cipher_suite_provider: &CSP,
575 public_tree: &TreeKemPublic,
576 psk_storage: &P,
577 user_rules: F,
578 ) -> Result<ProvisionalState, MlsError>
579 where
580 C: IdentityProvider,
581 F: MlsRules,
582 P: PreSharedKeyStorage,
583 CSP: CipherSuiteProvider,
584 {
585 let mut context =
586 get_test_group_context(123, cipher_suite_provider.cipher_suite()).await;
587
588 context.extensions = group_extensions.clone();
589
590 let mut state = GroupState::new(
591 context,
592 public_tree.clone(),
593 Vec::new().into(),
594 ConfirmationTag::empty(cipher_suite_provider).await,
595 );
596
597 state.proposals.proposals.clone_from(&self.proposals);
598 let proposals = self.resolve_for_commit(sender, proposal_list)?;
599
600 state
601 .apply_resolved(
602 sender,
603 proposals,
604 external_leaf,
605 identity_provider,
606 cipher_suite_provider,
607 psk_storage,
608 &user_rules,
609 None,
610 CommitDirection::Receive,
611 )
612 .await
613 }
614
615 #[allow(clippy::too_many_arguments)]
616 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
prepare_commit_default<C, F, P, CSP>( &self, sender: Sender, additional_proposals: Vec<Proposal>, context: &GroupContext, identity_provider: &C, cipher_suite_provider: &CSP, public_tree: &TreeKemPublic, external_leaf: Option<&LeafNode>, psk_storage: &P, user_rules: F, ) -> Result<ProvisionalState, MlsError> where C: IdentityProvider, F: MlsRules, P: PreSharedKeyStorage, CSP: CipherSuiteProvider,617 pub async fn prepare_commit_default<C, F, P, CSP>(
618 &self,
619 sender: Sender,
620 additional_proposals: Vec<Proposal>,
621 context: &GroupContext,
622 identity_provider: &C,
623 cipher_suite_provider: &CSP,
624 public_tree: &TreeKemPublic,
625 external_leaf: Option<&LeafNode>,
626 psk_storage: &P,
627 user_rules: F,
628 ) -> Result<ProvisionalState, MlsError>
629 where
630 C: IdentityProvider,
631 F: MlsRules,
632 P: PreSharedKeyStorage,
633 CSP: CipherSuiteProvider,
634 {
635 let state = GroupState::new(
636 context.clone(),
637 public_tree.clone(),
638 Vec::new().into(),
639 ConfirmationTag::empty(cipher_suite_provider).await,
640 );
641
642 let proposals = self.prepare_commit(sender, additional_proposals);
643
644 state
645 .apply_resolved(
646 sender,
647 proposals,
648 external_leaf,
649 identity_provider,
650 cipher_suite_provider,
651 psk_storage,
652 &user_rules,
653 None,
654 CommitDirection::Send,
655 )
656 .await
657 }
658 }
659 }
660
661 // TODO add tests for lite version of filtering
662 #[cfg(all(feature = "by_ref_proposal", test))]
663 mod tests {
664 use alloc::{boxed::Box, vec, vec::Vec};
665
666 use super::test_utils::{make_proposal_cache, pass_through_rules, CommitReceiver};
667 use super::{CachedProposal, ProposalCache};
668 use crate::client::MlsError;
669 use crate::group::message_processor::ProvisionalState;
670 use crate::group::mls_rules::{CommitDirection, CommitSource, EncryptionOptions};
671 use crate::group::proposal_filter::{ProposalBundle, ProposalInfo, ProposalSource};
672 use crate::group::proposal_ref::test_utils::auth_content_from_proposal;
673 use crate::group::proposal_ref::ProposalRef;
674 use crate::group::{
675 AddProposal, AuthenticatedContent, Content, ExternalInit, Proposal, ProposalOrRef,
676 ReInitProposal, RemoveProposal, Roster, Sender, UpdateProposal,
677 };
678 use crate::key_package::test_utils::test_key_package_with_signer;
679 use crate::signer::Signable;
680 use crate::tree_kem::leaf_node::LeafNode;
681 use crate::tree_kem::node::LeafIndex;
682 use crate::tree_kem::TreeKemPublic;
683 use crate::{
684 client::test_utils::{TEST_CIPHER_SUITE, TEST_PROTOCOL_VERSION},
685 crypto::{self, test_utils::test_cipher_suite_provider},
686 extension::test_utils::TestExtension,
687 group::{
688 message_processor::path_update_required,
689 proposal_filter::proposer_can_propose,
690 test_utils::{get_test_group_context, random_bytes, test_group, TEST_GROUP},
691 },
692 identity::basic::BasicIdentityProvider,
693 identity::test_utils::{get_test_signing_identity, BasicWithCustomProvider},
694 key_package::{test_utils::test_key_package, KeyPackageGenerator},
695 mls_rules::{CommitOptions, DefaultMlsRules},
696 psk::AlwaysFoundPskStorage,
697 tree_kem::{
698 leaf_node::{
699 test_utils::{
700 default_properties, get_basic_test_node, get_basic_test_node_capabilities,
701 get_basic_test_node_sig_key, get_test_capabilities,
702 },
703 ConfigProperties, LeafNodeSigningContext, LeafNodeSource,
704 },
705 Lifetime,
706 },
707 };
708 use crate::{KeyPackage, MlsRules};
709
710 use crate::extension::RequiredCapabilitiesExt;
711
712 #[cfg(feature = "by_ref_proposal")]
713 use crate::{
714 extension::ExternalSendersExt,
715 tree_kem::leaf_node_validator::test_utils::FailureIdentityProvider,
716 };
717
718 #[cfg(feature = "psk")]
719 use crate::{
720 group::proposal::PreSharedKeyProposal,
721 psk::{
722 ExternalPskId, JustPreSharedKeyID, PreSharedKeyID, PskGroupId, PskNonce,
723 ResumptionPSKUsage, ResumptionPsk,
724 },
725 };
726
727 #[cfg(feature = "custom_proposal")]
728 use crate::group::proposal::CustomProposal;
729
730 use assert_matches::assert_matches;
731 use core::convert::Infallible;
732 use itertools::Itertools;
733 use mls_rs_core::crypto::{CipherSuite, CipherSuiteProvider};
734 use mls_rs_core::extension::ExtensionList;
735 use mls_rs_core::group::{Capabilities, ProposalType};
736 use mls_rs_core::identity::IdentityProvider;
737 use mls_rs_core::protocol_version::ProtocolVersion;
738 use mls_rs_core::psk::{PreSharedKey, PreSharedKeyStorage};
739 use mls_rs_core::{
740 extension::MlsExtension,
741 identity::{Credential, CredentialType, CustomCredential},
742 };
743
test_sender() -> u32744 fn test_sender() -> u32 {
745 1
746 }
747
748 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
new_tree_custom_proposals( name: &str, proposal_types: Vec<ProposalType>, ) -> (LeafIndex, TreeKemPublic)749 async fn new_tree_custom_proposals(
750 name: &str,
751 proposal_types: Vec<ProposalType>,
752 ) -> (LeafIndex, TreeKemPublic) {
753 let (leaf, secret, _) = get_basic_test_node_capabilities(
754 TEST_CIPHER_SUITE,
755 name,
756 Capabilities {
757 proposals: proposal_types,
758 ..get_test_capabilities()
759 },
760 )
761 .await;
762
763 let (pub_tree, priv_tree) =
764 TreeKemPublic::derive(leaf, secret, &BasicIdentityProvider, &Default::default())
765 .await
766 .unwrap();
767
768 (priv_tree.self_index, pub_tree)
769 }
770
771 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
new_tree(name: &str) -> (LeafIndex, TreeKemPublic)772 async fn new_tree(name: &str) -> (LeafIndex, TreeKemPublic) {
773 new_tree_custom_proposals(name, vec![]).await
774 }
775
776 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
add_member(tree: &mut TreeKemPublic, name: &str) -> LeafIndex777 async fn add_member(tree: &mut TreeKemPublic, name: &str) -> LeafIndex {
778 let test_node = get_basic_test_node(TEST_CIPHER_SUITE, name).await;
779
780 tree.add_leaves(
781 vec![test_node],
782 &BasicIdentityProvider,
783 &test_cipher_suite_provider(TEST_CIPHER_SUITE),
784 )
785 .await
786 .unwrap()[0]
787 }
788
789 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
update_leaf_node(name: &str, leaf_index: u32) -> LeafNode790 async fn update_leaf_node(name: &str, leaf_index: u32) -> LeafNode {
791 let (mut leaf, _, signer) = get_basic_test_node_sig_key(TEST_CIPHER_SUITE, name).await;
792
793 leaf.update(
794 &test_cipher_suite_provider(TEST_CIPHER_SUITE),
795 TEST_GROUP,
796 leaf_index,
797 default_properties(),
798 None,
799 &signer,
800 )
801 .await
802 .unwrap();
803
804 leaf
805 }
806
807 struct TestProposals {
808 test_sender: u32,
809 test_proposals: Vec<AuthenticatedContent>,
810 expected_effects: ProvisionalState,
811 tree: TreeKemPublic,
812 }
813
814 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
test_proposals( protocol_version: ProtocolVersion, cipher_suite: CipherSuite, ) -> TestProposals815 async fn test_proposals(
816 protocol_version: ProtocolVersion,
817 cipher_suite: CipherSuite,
818 ) -> TestProposals {
819 let cipher_suite_provider = test_cipher_suite_provider(cipher_suite);
820
821 let (sender_leaf, sender_leaf_secret, _) =
822 get_basic_test_node_sig_key(cipher_suite, "alice").await;
823
824 let sender = LeafIndex(0);
825
826 let (mut tree, _) = TreeKemPublic::derive(
827 sender_leaf,
828 sender_leaf_secret,
829 &BasicIdentityProvider,
830 &Default::default(),
831 )
832 .await
833 .unwrap();
834
835 let add_package = test_key_package(protocol_version, cipher_suite, "dave").await;
836
837 let remove_leaf_index = add_member(&mut tree, "carol").await;
838
839 let add = Proposal::Add(Box::new(AddProposal {
840 key_package: add_package.clone(),
841 }));
842
843 let remove = Proposal::Remove(RemoveProposal {
844 to_remove: remove_leaf_index,
845 });
846
847 let extensions = Proposal::GroupContextExtensions(ExtensionList::new());
848
849 let proposals = vec![add, remove, extensions];
850
851 let test_node = get_basic_test_node(cipher_suite, "charlie").await;
852
853 let test_sender = *tree
854 .add_leaves(
855 vec![test_node],
856 &BasicIdentityProvider,
857 &cipher_suite_provider,
858 )
859 .await
860 .unwrap()[0];
861
862 let mut expected_tree = tree.clone();
863
864 let mut bundle = ProposalBundle::default();
865
866 let plaintext = proposals
867 .iter()
868 .cloned()
869 .map(|p| auth_content_from_proposal(p, sender))
870 .collect_vec();
871
872 for i in 0..proposals.len() {
873 let pref = ProposalRef::from_content(&cipher_suite_provider, &plaintext[i])
874 .await
875 .unwrap();
876
877 bundle.add(
878 proposals[i].clone(),
879 Sender::Member(test_sender),
880 ProposalSource::ByReference(pref),
881 )
882 }
883
884 expected_tree
885 .batch_edit(
886 &mut bundle,
887 &Default::default(),
888 &BasicIdentityProvider,
889 &cipher_suite_provider,
890 true,
891 )
892 .await
893 .unwrap();
894
895 let expected_effects = ProvisionalState {
896 public_tree: expected_tree,
897 group_context: get_test_group_context(1, cipher_suite).await,
898 external_init_index: None,
899 indexes_of_added_kpkgs: vec![LeafIndex(1)],
900 #[cfg(feature = "state_update")]
901 unused_proposals: vec![],
902 applied_proposals: bundle,
903 };
904
905 TestProposals {
906 test_sender,
907 test_proposals: plaintext,
908 expected_effects,
909 tree,
910 }
911 }
912
913 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
filter_proposals( cipher_suite: CipherSuite, proposals: Vec<AuthenticatedContent>, ) -> Vec<(ProposalRef, CachedProposal)>914 async fn filter_proposals(
915 cipher_suite: CipherSuite,
916 proposals: Vec<AuthenticatedContent>,
917 ) -> Vec<(ProposalRef, CachedProposal)> {
918 let mut contents = Vec::new();
919
920 for p in proposals {
921 if let Content::Proposal(proposal) = &p.content.content {
922 let proposal_ref =
923 ProposalRef::from_content(&test_cipher_suite_provider(cipher_suite), &p)
924 .await
925 .unwrap();
926 contents.push((
927 proposal_ref,
928 CachedProposal::new(proposal.as_ref().clone(), p.content.sender),
929 ));
930 }
931 }
932
933 contents
934 }
935
936 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
make_proposal_ref<S>(p: &Proposal, sender: S) -> ProposalRef where S: Into<Sender>,937 async fn make_proposal_ref<S>(p: &Proposal, sender: S) -> ProposalRef
938 where
939 S: Into<Sender>,
940 {
941 ProposalRef::from_content(
942 &test_cipher_suite_provider(TEST_CIPHER_SUITE),
943 &auth_content_from_proposal(p.clone(), sender),
944 )
945 .await
946 .unwrap()
947 }
948
949 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
make_proposal_info<S>(p: &Proposal, sender: S) -> ProposalInfo<Proposal> where S: Into<Sender> + Clone,950 async fn make_proposal_info<S>(p: &Proposal, sender: S) -> ProposalInfo<Proposal>
951 where
952 S: Into<Sender> + Clone,
953 {
954 ProposalInfo {
955 proposal: p.clone(),
956 sender: sender.clone().into(),
957 source: ProposalSource::ByReference(make_proposal_ref(p, sender).await),
958 }
959 }
960
961 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
test_proposal_cache_setup(proposals: Vec<AuthenticatedContent>) -> ProposalCache962 async fn test_proposal_cache_setup(proposals: Vec<AuthenticatedContent>) -> ProposalCache {
963 let mut cache = make_proposal_cache();
964 cache.extend(filter_proposals(TEST_CIPHER_SUITE, proposals).await);
965 cache
966 }
967
assert_matches(mut expected_state: ProvisionalState, state: ProvisionalState)968 fn assert_matches(mut expected_state: ProvisionalState, state: ProvisionalState) {
969 let expected_proposals = expected_state.applied_proposals.into_proposals_or_refs();
970 let proposals = state.applied_proposals.into_proposals_or_refs();
971
972 assert_eq!(proposals.len(), expected_proposals.len());
973
974 // Determine there are no duplicates in the proposals returned
975 assert!(!proposals.iter().enumerate().any(|(i, p1)| proposals
976 .iter()
977 .enumerate()
978 .any(|(j, p2)| p1 == p2 && i != j)),);
979
980 // Proposal order may change so we just compare the length and contents are the same
981 expected_proposals
982 .iter()
983 .for_each(|p| assert!(proposals.contains(p)));
984
985 assert_eq!(
986 expected_state.external_init_index,
987 state.external_init_index
988 );
989
990 // We don't compare the epoch in this test.
991 expected_state.group_context.epoch = state.group_context.epoch;
992 assert_eq!(expected_state.group_context, state.group_context);
993
994 assert_eq!(
995 expected_state.indexes_of_added_kpkgs,
996 state.indexes_of_added_kpkgs
997 );
998
999 assert_eq!(expected_state.public_tree, state.public_tree);
1000
1001 #[cfg(feature = "state_update")]
1002 assert_eq!(expected_state.unused_proposals, state.unused_proposals);
1003 }
1004
1005 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
test_proposal_cache_commit_all_cached()1006 async fn test_proposal_cache_commit_all_cached() {
1007 let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
1008
1009 let TestProposals {
1010 test_sender,
1011 test_proposals,
1012 expected_effects,
1013 tree,
1014 ..
1015 } = test_proposals(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1016
1017 let cache = test_proposal_cache_setup(test_proposals.clone()).await;
1018
1019 let provisional_state = cache
1020 .prepare_commit_default(
1021 Sender::Member(test_sender),
1022 vec![],
1023 &get_test_group_context(0, TEST_CIPHER_SUITE).await,
1024 &BasicIdentityProvider,
1025 &cipher_suite_provider,
1026 &tree,
1027 None,
1028 &AlwaysFoundPskStorage,
1029 pass_through_rules(),
1030 )
1031 .await
1032 .unwrap();
1033
1034 assert_matches(expected_effects, provisional_state)
1035 }
1036
1037 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
test_proposal_cache_commit_additional()1038 async fn test_proposal_cache_commit_additional() {
1039 let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
1040
1041 let TestProposals {
1042 test_sender,
1043 test_proposals,
1044 mut expected_effects,
1045 tree,
1046 ..
1047 } = test_proposals(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1048
1049 let additional_key_package =
1050 test_key_package(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "frank").await;
1051
1052 let additional = AddProposal {
1053 key_package: additional_key_package.clone(),
1054 };
1055
1056 let cache = test_proposal_cache_setup(test_proposals.clone()).await;
1057
1058 let provisional_state = cache
1059 .prepare_commit_default(
1060 Sender::Member(test_sender),
1061 vec![Proposal::Add(Box::new(additional.clone()))],
1062 &get_test_group_context(0, TEST_CIPHER_SUITE).await,
1063 &BasicIdentityProvider,
1064 &cipher_suite_provider,
1065 &tree,
1066 None,
1067 &AlwaysFoundPskStorage,
1068 pass_through_rules(),
1069 )
1070 .await
1071 .unwrap();
1072
1073 expected_effects.applied_proposals.add(
1074 Proposal::Add(Box::new(additional.clone())),
1075 Sender::Member(test_sender),
1076 ProposalSource::ByValue,
1077 );
1078
1079 let leaf = vec![additional_key_package.leaf_node.clone()];
1080
1081 expected_effects
1082 .public_tree
1083 .add_leaves(leaf, &BasicIdentityProvider, &cipher_suite_provider)
1084 .await
1085 .unwrap();
1086
1087 expected_effects.indexes_of_added_kpkgs.push(LeafIndex(3));
1088
1089 assert_matches(expected_effects, provisional_state);
1090 }
1091
1092 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
test_proposal_cache_update_filter()1093 async fn test_proposal_cache_update_filter() {
1094 let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
1095
1096 let TestProposals {
1097 test_proposals,
1098 tree,
1099 ..
1100 } = test_proposals(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1101
1102 let update_proposal = make_update_proposal("foo").await;
1103
1104 let additional = vec![Proposal::Update(update_proposal)];
1105
1106 let cache = test_proposal_cache_setup(test_proposals).await;
1107
1108 let res = cache
1109 .prepare_commit_default(
1110 Sender::Member(test_sender()),
1111 additional,
1112 &get_test_group_context(0, TEST_CIPHER_SUITE).await,
1113 &BasicIdentityProvider,
1114 &cipher_suite_provider,
1115 &tree,
1116 None,
1117 &AlwaysFoundPskStorage,
1118 pass_through_rules(),
1119 )
1120 .await;
1121
1122 assert_matches!(res, Err(MlsError::InvalidProposalTypeForSender));
1123 }
1124
1125 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
test_proposal_cache_removal_override_update()1126 async fn test_proposal_cache_removal_override_update() {
1127 let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
1128
1129 let TestProposals {
1130 test_sender,
1131 test_proposals,
1132 tree,
1133 ..
1134 } = test_proposals(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1135
1136 let update = Proposal::Update(make_update_proposal("foo").await);
1137 let update_proposal_ref = make_proposal_ref(&update, LeafIndex(1)).await;
1138 let mut cache = test_proposal_cache_setup(test_proposals).await;
1139
1140 cache.insert(update_proposal_ref.clone(), update, Sender::Member(1));
1141
1142 let provisional_state = cache
1143 .prepare_commit_default(
1144 Sender::Member(test_sender),
1145 vec![],
1146 &get_test_group_context(0, TEST_CIPHER_SUITE).await,
1147 &BasicIdentityProvider,
1148 &cipher_suite_provider,
1149 &tree,
1150 None,
1151 &AlwaysFoundPskStorage,
1152 pass_through_rules(),
1153 )
1154 .await
1155 .unwrap();
1156
1157 assert!(provisional_state
1158 .applied_proposals
1159 .removals
1160 .iter()
1161 .any(|p| *p.proposal.to_remove == 1));
1162
1163 assert!(!provisional_state
1164 .applied_proposals
1165 .into_proposals_or_refs()
1166 .contains(&ProposalOrRef::Reference(update_proposal_ref)))
1167 }
1168
1169 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
test_proposal_cache_filter_duplicates_insert()1170 async fn test_proposal_cache_filter_duplicates_insert() {
1171 let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
1172
1173 let TestProposals {
1174 test_sender,
1175 test_proposals,
1176 expected_effects,
1177 tree,
1178 ..
1179 } = test_proposals(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1180
1181 let mut cache = test_proposal_cache_setup(test_proposals.clone()).await;
1182 cache.extend(filter_proposals(TEST_CIPHER_SUITE, test_proposals.clone()).await);
1183
1184 let provisional_state = cache
1185 .prepare_commit_default(
1186 Sender::Member(test_sender),
1187 vec![],
1188 &get_test_group_context(0, TEST_CIPHER_SUITE).await,
1189 &BasicIdentityProvider,
1190 &cipher_suite_provider,
1191 &tree,
1192 None,
1193 &AlwaysFoundPskStorage,
1194 pass_through_rules(),
1195 )
1196 .await
1197 .unwrap();
1198
1199 assert_matches(expected_effects, provisional_state)
1200 }
1201
1202 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
test_proposal_cache_filter_duplicates_additional()1203 async fn test_proposal_cache_filter_duplicates_additional() {
1204 let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
1205
1206 let TestProposals {
1207 test_proposals,
1208 expected_effects,
1209 tree,
1210 ..
1211 } = test_proposals(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1212
1213 let mut cache = test_proposal_cache_setup(test_proposals.clone()).await;
1214
1215 // Updates from different senders will be allowed so we test duplicates for add / remove
1216 let additional = test_proposals
1217 .clone()
1218 .into_iter()
1219 .filter_map(|plaintext| match plaintext.content.content {
1220 Content::Proposal(p) if p.proposal_type() == ProposalType::UPDATE => None,
1221 Content::Proposal(_) => Some(plaintext),
1222 _ => None,
1223 })
1224 .collect::<Vec<_>>();
1225
1226 cache.extend(filter_proposals(TEST_CIPHER_SUITE, additional).await);
1227
1228 let provisional_state = cache
1229 .prepare_commit_default(
1230 Sender::Member(2),
1231 Vec::new(),
1232 &get_test_group_context(0, TEST_CIPHER_SUITE).await,
1233 &BasicIdentityProvider,
1234 &cipher_suite_provider,
1235 &tree,
1236 None,
1237 &AlwaysFoundPskStorage,
1238 pass_through_rules(),
1239 )
1240 .await
1241 .unwrap();
1242
1243 assert_matches(expected_effects, provisional_state)
1244 }
1245
1246 #[cfg(feature = "private_message")]
1247 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
test_proposal_cache_is_empty()1248 async fn test_proposal_cache_is_empty() {
1249 let mut cache = make_proposal_cache();
1250 assert!(cache.is_empty());
1251
1252 let test_proposal = Proposal::Remove(RemoveProposal {
1253 to_remove: LeafIndex(test_sender()),
1254 });
1255
1256 let proposer = test_sender();
1257 let test_proposal_ref = make_proposal_ref(&test_proposal, LeafIndex(proposer)).await;
1258 cache.insert(test_proposal_ref, test_proposal, Sender::Member(proposer));
1259
1260 assert!(!cache.is_empty())
1261 }
1262
1263 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
test_proposal_cache_resolve()1264 async fn test_proposal_cache_resolve() {
1265 let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
1266
1267 let TestProposals {
1268 test_sender,
1269 test_proposals,
1270 tree,
1271 ..
1272 } = test_proposals(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1273
1274 let cache = test_proposal_cache_setup(test_proposals).await;
1275
1276 let proposal = Proposal::Add(Box::new(AddProposal {
1277 key_package: test_key_package(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "frank").await,
1278 }));
1279
1280 let additional = vec![proposal];
1281
1282 let expected_effects = cache
1283 .prepare_commit_default(
1284 Sender::Member(test_sender),
1285 additional,
1286 &get_test_group_context(0, TEST_CIPHER_SUITE).await,
1287 &BasicIdentityProvider,
1288 &cipher_suite_provider,
1289 &tree,
1290 None,
1291 &AlwaysFoundPskStorage,
1292 pass_through_rules(),
1293 )
1294 .await
1295 .unwrap();
1296
1297 let proposals = expected_effects
1298 .applied_proposals
1299 .clone()
1300 .into_proposals_or_refs();
1301
1302 let resolution = cache
1303 .resolve_for_commit_default(
1304 Sender::Member(test_sender),
1305 proposals,
1306 None,
1307 &ExtensionList::new(),
1308 &BasicIdentityProvider,
1309 &cipher_suite_provider,
1310 &tree,
1311 &AlwaysFoundPskStorage,
1312 pass_through_rules(),
1313 )
1314 .await
1315 .unwrap();
1316
1317 assert_matches(expected_effects, resolution);
1318 }
1319
1320 #[cfg(feature = "psk")]
1321 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
proposal_cache_filters_duplicate_psk_ids()1322 async fn proposal_cache_filters_duplicate_psk_ids() {
1323 let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
1324
1325 let (alice, tree) = new_tree("alice").await;
1326 let cache = make_proposal_cache();
1327
1328 let proposal = Proposal::Psk(make_external_psk(
1329 b"ted",
1330 crate::psk::PskNonce::random(&test_cipher_suite_provider(TEST_CIPHER_SUITE)).unwrap(),
1331 ));
1332
1333 let res = cache
1334 .prepare_commit_default(
1335 Sender::Member(*alice),
1336 vec![proposal.clone(), proposal],
1337 &get_test_group_context(0, TEST_CIPHER_SUITE).await,
1338 &BasicIdentityProvider,
1339 &cipher_suite_provider,
1340 &tree,
1341 None,
1342 &AlwaysFoundPskStorage,
1343 pass_through_rules(),
1344 )
1345 .await;
1346
1347 assert_matches!(res, Err(MlsError::DuplicatePskIds));
1348 }
1349
1350 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
test_node() -> LeafNode1351 async fn test_node() -> LeafNode {
1352 let (mut leaf_node, _, signer) =
1353 get_basic_test_node_sig_key(TEST_CIPHER_SUITE, "foo").await;
1354
1355 leaf_node
1356 .commit(
1357 &test_cipher_suite_provider(TEST_CIPHER_SUITE),
1358 TEST_GROUP,
1359 0,
1360 default_properties(),
1361 None,
1362 &signer,
1363 )
1364 .await
1365 .unwrap();
1366
1367 leaf_node
1368 }
1369
1370 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
external_commit_must_have_new_leaf()1371 async fn external_commit_must_have_new_leaf() {
1372 let cache = make_proposal_cache();
1373 let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
1374
1375 let kem_output = vec![0; cipher_suite_provider.kdf_extract_size()];
1376 let group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1377 let public_tree = &group.group.state.public_tree;
1378
1379 let res = cache
1380 .resolve_for_commit_default(
1381 Sender::NewMemberCommit,
1382 vec![ProposalOrRef::Proposal(Box::new(Proposal::ExternalInit(
1383 ExternalInit { kem_output },
1384 )))],
1385 None,
1386 &group.group.context().extensions,
1387 &BasicIdentityProvider,
1388 &cipher_suite_provider,
1389 public_tree,
1390 &AlwaysFoundPskStorage,
1391 pass_through_rules(),
1392 )
1393 .await;
1394
1395 assert_matches!(res, Err(MlsError::ExternalCommitMustHaveNewLeaf));
1396 }
1397
1398 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
proposal_cache_rejects_proposals_by_ref_for_new_member()1399 async fn proposal_cache_rejects_proposals_by_ref_for_new_member() {
1400 let mut cache = make_proposal_cache();
1401 let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
1402
1403 let proposal = {
1404 let kem_output = vec![0; cipher_suite_provider.kdf_extract_size()];
1405 Proposal::ExternalInit(ExternalInit { kem_output })
1406 };
1407
1408 let proposal_ref = make_proposal_ref(&proposal, test_sender()).await;
1409
1410 cache.insert(
1411 proposal_ref.clone(),
1412 proposal,
1413 Sender::Member(test_sender()),
1414 );
1415
1416 let group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1417 let public_tree = &group.group.state.public_tree;
1418
1419 let res = cache
1420 .resolve_for_commit_default(
1421 Sender::NewMemberCommit,
1422 vec![ProposalOrRef::Reference(proposal_ref)],
1423 Some(&test_node().await),
1424 &group.group.context().extensions,
1425 &BasicIdentityProvider,
1426 &cipher_suite_provider,
1427 public_tree,
1428 &AlwaysFoundPskStorage,
1429 pass_through_rules(),
1430 )
1431 .await;
1432
1433 assert_matches!(res, Err(MlsError::OnlyMembersCanCommitProposalsByRef));
1434 }
1435
1436 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
proposal_cache_rejects_multiple_external_init_proposals_in_commit()1437 async fn proposal_cache_rejects_multiple_external_init_proposals_in_commit() {
1438 let cache = make_proposal_cache();
1439 let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
1440 let kem_output = vec![0; cipher_suite_provider.kdf_extract_size()];
1441 let group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1442 let public_tree = &group.group.state.public_tree;
1443
1444 let res = cache
1445 .resolve_for_commit_default(
1446 Sender::NewMemberCommit,
1447 [
1448 Proposal::ExternalInit(ExternalInit {
1449 kem_output: kem_output.clone(),
1450 }),
1451 Proposal::ExternalInit(ExternalInit { kem_output }),
1452 ]
1453 .into_iter()
1454 .map(|p| ProposalOrRef::Proposal(Box::new(p)))
1455 .collect(),
1456 Some(&test_node().await),
1457 &group.group.context().extensions,
1458 &BasicIdentityProvider,
1459 &cipher_suite_provider,
1460 public_tree,
1461 &AlwaysFoundPskStorage,
1462 pass_through_rules(),
1463 )
1464 .await;
1465
1466 assert_matches!(
1467 res,
1468 Err(MlsError::ExternalCommitMustHaveExactlyOneExternalInit)
1469 );
1470 }
1471
1472 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
new_member_commits_proposal(proposal: Proposal) -> Result<ProvisionalState, MlsError>1473 async fn new_member_commits_proposal(proposal: Proposal) -> Result<ProvisionalState, MlsError> {
1474 let cache = make_proposal_cache();
1475 let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
1476 let kem_output = vec![0; cipher_suite_provider.kdf_extract_size()];
1477 let group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1478 let public_tree = &group.group.state.public_tree;
1479
1480 cache
1481 .resolve_for_commit_default(
1482 Sender::NewMemberCommit,
1483 [
1484 Proposal::ExternalInit(ExternalInit { kem_output }),
1485 proposal,
1486 ]
1487 .into_iter()
1488 .map(|p| ProposalOrRef::Proposal(Box::new(p)))
1489 .collect(),
1490 Some(&test_node().await),
1491 &group.group.context().extensions,
1492 &BasicIdentityProvider,
1493 &cipher_suite_provider,
1494 public_tree,
1495 &AlwaysFoundPskStorage,
1496 pass_through_rules(),
1497 )
1498 .await
1499 }
1500
1501 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
new_member_cannot_commit_add_proposal()1502 async fn new_member_cannot_commit_add_proposal() {
1503 let res = new_member_commits_proposal(Proposal::Add(Box::new(AddProposal {
1504 key_package: test_key_package(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "frank").await,
1505 })))
1506 .await;
1507
1508 assert_matches!(
1509 res,
1510 Err(MlsError::InvalidProposalTypeInExternalCommit(
1511 ProposalType::ADD
1512 ))
1513 );
1514 }
1515
1516 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
new_member_cannot_commit_more_than_one_remove_proposal()1517 async fn new_member_cannot_commit_more_than_one_remove_proposal() {
1518 let cache = make_proposal_cache();
1519 let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
1520 let kem_output = vec![0; cipher_suite_provider.kdf_extract_size()];
1521 let group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1522 let group_extensions = group.group.context().extensions.clone();
1523 let mut public_tree = group.group.state.public_tree;
1524
1525 let foo = get_basic_test_node(TEST_CIPHER_SUITE, "foo").await;
1526
1527 let bar = get_basic_test_node(TEST_CIPHER_SUITE, "bar").await;
1528
1529 let test_leaf_nodes = vec![foo, bar];
1530
1531 let test_leaf_node_indexes = public_tree
1532 .add_leaves(
1533 test_leaf_nodes,
1534 &BasicIdentityProvider,
1535 &cipher_suite_provider,
1536 )
1537 .await
1538 .unwrap();
1539
1540 let proposals = vec![
1541 Proposal::ExternalInit(ExternalInit { kem_output }),
1542 Proposal::Remove(RemoveProposal {
1543 to_remove: test_leaf_node_indexes[0],
1544 }),
1545 Proposal::Remove(RemoveProposal {
1546 to_remove: test_leaf_node_indexes[1],
1547 }),
1548 ];
1549
1550 let res = cache
1551 .resolve_for_commit_default(
1552 Sender::NewMemberCommit,
1553 proposals
1554 .into_iter()
1555 .map(|p| ProposalOrRef::Proposal(Box::new(p)))
1556 .collect(),
1557 Some(&test_node().await),
1558 &group_extensions,
1559 &BasicIdentityProvider,
1560 &cipher_suite_provider,
1561 &public_tree,
1562 &AlwaysFoundPskStorage,
1563 pass_through_rules(),
1564 )
1565 .await;
1566
1567 assert_matches!(res, Err(MlsError::ExternalCommitWithMoreThanOneRemove));
1568 }
1569
1570 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
new_member_remove_proposal_invalid_credential()1571 async fn new_member_remove_proposal_invalid_credential() {
1572 let cache = make_proposal_cache();
1573 let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
1574 let kem_output = vec![0; cipher_suite_provider.kdf_extract_size()];
1575 let group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1576 let group_extensions = group.group.context().extensions.clone();
1577 let mut public_tree = group.group.state.public_tree;
1578
1579 let node = get_basic_test_node(TEST_CIPHER_SUITE, "bar").await;
1580
1581 let test_leaf_nodes = vec![node];
1582
1583 let test_leaf_node_indexes = public_tree
1584 .add_leaves(
1585 test_leaf_nodes,
1586 &BasicIdentityProvider,
1587 &cipher_suite_provider,
1588 )
1589 .await
1590 .unwrap();
1591
1592 let proposals = vec![
1593 Proposal::ExternalInit(ExternalInit { kem_output }),
1594 Proposal::Remove(RemoveProposal {
1595 to_remove: test_leaf_node_indexes[0],
1596 }),
1597 ];
1598
1599 let res = cache
1600 .resolve_for_commit_default(
1601 Sender::NewMemberCommit,
1602 proposals
1603 .into_iter()
1604 .map(|p| ProposalOrRef::Proposal(Box::new(p)))
1605 .collect(),
1606 Some(&test_node().await),
1607 &group_extensions,
1608 &BasicIdentityProvider,
1609 &cipher_suite_provider,
1610 &public_tree,
1611 &AlwaysFoundPskStorage,
1612 pass_through_rules(),
1613 )
1614 .await;
1615
1616 assert_matches!(res, Err(MlsError::ExternalCommitRemovesOtherIdentity));
1617 }
1618
1619 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
new_member_remove_proposal_valid_credential()1620 async fn new_member_remove_proposal_valid_credential() {
1621 let cache = make_proposal_cache();
1622 let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
1623 let kem_output = vec![0; cipher_suite_provider.kdf_extract_size()];
1624 let group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1625 let group_extensions = group.group.context().extensions.clone();
1626 let mut public_tree = group.group.state.public_tree;
1627
1628 let node = get_basic_test_node(TEST_CIPHER_SUITE, "foo").await;
1629
1630 let test_leaf_nodes = vec![node];
1631
1632 let test_leaf_node_indexes = public_tree
1633 .add_leaves(
1634 test_leaf_nodes,
1635 &BasicIdentityProvider,
1636 &cipher_suite_provider,
1637 )
1638 .await
1639 .unwrap();
1640
1641 let proposals = vec![
1642 Proposal::ExternalInit(ExternalInit { kem_output }),
1643 Proposal::Remove(RemoveProposal {
1644 to_remove: test_leaf_node_indexes[0],
1645 }),
1646 ];
1647
1648 let res = cache
1649 .resolve_for_commit_default(
1650 Sender::NewMemberCommit,
1651 proposals
1652 .into_iter()
1653 .map(|p| ProposalOrRef::Proposal(Box::new(p)))
1654 .collect(),
1655 Some(&test_node().await),
1656 &group_extensions,
1657 &BasicIdentityProvider,
1658 &cipher_suite_provider,
1659 &public_tree,
1660 &AlwaysFoundPskStorage,
1661 pass_through_rules(),
1662 )
1663 .await;
1664
1665 assert_matches!(res, Ok(_));
1666 }
1667
1668 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
new_member_cannot_commit_update_proposal()1669 async fn new_member_cannot_commit_update_proposal() {
1670 let res = new_member_commits_proposal(Proposal::Update(UpdateProposal {
1671 leaf_node: get_basic_test_node(TEST_CIPHER_SUITE, "foo").await,
1672 }))
1673 .await;
1674
1675 assert_matches!(
1676 res,
1677 Err(MlsError::InvalidProposalTypeInExternalCommit(
1678 ProposalType::UPDATE
1679 ))
1680 );
1681 }
1682
1683 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
new_member_cannot_commit_group_extensions_proposal()1684 async fn new_member_cannot_commit_group_extensions_proposal() {
1685 let res =
1686 new_member_commits_proposal(Proposal::GroupContextExtensions(ExtensionList::new()))
1687 .await;
1688
1689 assert_matches!(
1690 res,
1691 Err(MlsError::InvalidProposalTypeInExternalCommit(
1692 ProposalType::GROUP_CONTEXT_EXTENSIONS,
1693 ))
1694 );
1695 }
1696
1697 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
new_member_cannot_commit_reinit_proposal()1698 async fn new_member_cannot_commit_reinit_proposal() {
1699 let res = new_member_commits_proposal(Proposal::ReInit(ReInitProposal {
1700 group_id: b"foo".to_vec(),
1701 version: TEST_PROTOCOL_VERSION,
1702 cipher_suite: TEST_CIPHER_SUITE,
1703 extensions: ExtensionList::new(),
1704 }))
1705 .await;
1706
1707 assert_matches!(
1708 res,
1709 Err(MlsError::InvalidProposalTypeInExternalCommit(
1710 ProposalType::RE_INIT
1711 ))
1712 );
1713 }
1714
1715 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
new_member_commit_must_contain_an_external_init_proposal()1716 async fn new_member_commit_must_contain_an_external_init_proposal() {
1717 let cache = make_proposal_cache();
1718 let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
1719 let group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1720 let public_tree = &group.group.state.public_tree;
1721
1722 let res = cache
1723 .resolve_for_commit_default(
1724 Sender::NewMemberCommit,
1725 Vec::new(),
1726 Some(&test_node().await),
1727 &group.group.context().extensions,
1728 &BasicIdentityProvider,
1729 &cipher_suite_provider,
1730 public_tree,
1731 &AlwaysFoundPskStorage,
1732 pass_through_rules(),
1733 )
1734 .await;
1735
1736 assert_matches!(
1737 res,
1738 Err(MlsError::ExternalCommitMustHaveExactlyOneExternalInit)
1739 );
1740 }
1741
1742 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
test_path_update_required_empty()1743 async fn test_path_update_required_empty() {
1744 let cache = make_proposal_cache();
1745 let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
1746
1747 let mut tree = TreeKemPublic::new();
1748 add_member(&mut tree, "alice").await;
1749 add_member(&mut tree, "bob").await;
1750
1751 let effects = cache
1752 .prepare_commit_default(
1753 Sender::Member(test_sender()),
1754 vec![],
1755 &get_test_group_context(1, TEST_CIPHER_SUITE).await,
1756 &BasicIdentityProvider,
1757 &cipher_suite_provider,
1758 &tree,
1759 None,
1760 &AlwaysFoundPskStorage,
1761 pass_through_rules(),
1762 )
1763 .await
1764 .unwrap();
1765
1766 assert!(path_update_required(&effects.applied_proposals))
1767 }
1768
1769 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
test_path_update_required_updates()1770 async fn test_path_update_required_updates() {
1771 let mut cache = make_proposal_cache();
1772 let update = Proposal::Update(make_update_proposal("bar").await);
1773 let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
1774
1775 cache.insert(
1776 make_proposal_ref(&update, LeafIndex(2)).await,
1777 update,
1778 Sender::Member(2),
1779 );
1780
1781 let mut tree = TreeKemPublic::new();
1782 add_member(&mut tree, "alice").await;
1783 add_member(&mut tree, "bob").await;
1784 add_member(&mut tree, "carol").await;
1785
1786 let effects = cache
1787 .prepare_commit_default(
1788 Sender::Member(test_sender()),
1789 Vec::new(),
1790 &get_test_group_context(1, TEST_CIPHER_SUITE).await,
1791 &BasicIdentityProvider,
1792 &cipher_suite_provider,
1793 &tree,
1794 None,
1795 &AlwaysFoundPskStorage,
1796 pass_through_rules(),
1797 )
1798 .await
1799 .unwrap();
1800
1801 assert!(path_update_required(&effects.applied_proposals))
1802 }
1803
1804 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
test_path_update_required_removes()1805 async fn test_path_update_required_removes() {
1806 let cache = make_proposal_cache();
1807 let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
1808
1809 let (alice_leaf, alice_secret, _) =
1810 get_basic_test_node_sig_key(TEST_CIPHER_SUITE, "alice").await;
1811 let alice = 0;
1812
1813 let (mut tree, _) = TreeKemPublic::derive(
1814 alice_leaf,
1815 alice_secret,
1816 &BasicIdentityProvider,
1817 &Default::default(),
1818 )
1819 .await
1820 .unwrap();
1821
1822 let bob_node = get_basic_test_node(TEST_CIPHER_SUITE, "bob").await;
1823
1824 let bob = tree
1825 .add_leaves(
1826 vec![bob_node],
1827 &BasicIdentityProvider,
1828 &cipher_suite_provider,
1829 )
1830 .await
1831 .unwrap()[0];
1832
1833 let remove = Proposal::Remove(RemoveProposal { to_remove: bob });
1834
1835 let effects = cache
1836 .prepare_commit_default(
1837 Sender::Member(alice),
1838 vec![remove],
1839 &get_test_group_context(1, TEST_CIPHER_SUITE).await,
1840 &BasicIdentityProvider,
1841 &cipher_suite_provider,
1842 &tree,
1843 None,
1844 &AlwaysFoundPskStorage,
1845 pass_through_rules(),
1846 )
1847 .await
1848 .unwrap();
1849
1850 assert!(path_update_required(&effects.applied_proposals))
1851 }
1852
1853 #[cfg(feature = "psk")]
1854 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
test_path_update_not_required()1855 async fn test_path_update_not_required() {
1856 let (alice, tree) = new_tree("alice").await;
1857 let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
1858 let cache = make_proposal_cache();
1859
1860 let psk = Proposal::Psk(PreSharedKeyProposal {
1861 psk: PreSharedKeyID::new(
1862 JustPreSharedKeyID::External(ExternalPskId::new(vec![])),
1863 &test_cipher_suite_provider(TEST_CIPHER_SUITE),
1864 )
1865 .unwrap(),
1866 });
1867
1868 let add = Proposal::Add(Box::new(AddProposal {
1869 key_package: test_key_package(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "bob").await,
1870 }));
1871
1872 let effects = cache
1873 .prepare_commit_default(
1874 Sender::Member(*alice),
1875 vec![psk, add],
1876 &get_test_group_context(1, TEST_CIPHER_SUITE).await,
1877 &BasicIdentityProvider,
1878 &cipher_suite_provider,
1879 &tree,
1880 None,
1881 &AlwaysFoundPskStorage,
1882 pass_through_rules(),
1883 )
1884 .await
1885 .unwrap();
1886
1887 assert!(!path_update_required(&effects.applied_proposals))
1888 }
1889
1890 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
path_update_is_not_required_for_re_init()1891 async fn path_update_is_not_required_for_re_init() {
1892 let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
1893 let (alice, tree) = new_tree("alice").await;
1894 let cache = make_proposal_cache();
1895
1896 let reinit = Proposal::ReInit(ReInitProposal {
1897 group_id: vec![],
1898 version: TEST_PROTOCOL_VERSION,
1899 cipher_suite: TEST_CIPHER_SUITE,
1900 extensions: Default::default(),
1901 });
1902
1903 let effects = cache
1904 .prepare_commit_default(
1905 Sender::Member(*alice),
1906 vec![reinit],
1907 &get_test_group_context(1, TEST_CIPHER_SUITE).await,
1908 &BasicIdentityProvider,
1909 &cipher_suite_provider,
1910 &tree,
1911 None,
1912 &AlwaysFoundPskStorage,
1913 pass_through_rules(),
1914 )
1915 .await
1916 .unwrap();
1917
1918 assert!(!path_update_required(&effects.applied_proposals))
1919 }
1920
1921 #[derive(Debug)]
1922 struct CommitSender<'a, C, F, P, CSP> {
1923 cipher_suite_provider: CSP,
1924 tree: &'a TreeKemPublic,
1925 sender: LeafIndex,
1926 cache: ProposalCache,
1927 additional_proposals: Vec<Proposal>,
1928 identity_provider: C,
1929 user_rules: F,
1930 psk_storage: P,
1931 }
1932
1933 impl<'a, CSP>
1934 CommitSender<'a, BasicWithCustomProvider, DefaultMlsRules, AlwaysFoundPskStorage, CSP>
1935 {
new(tree: &'a TreeKemPublic, sender: LeafIndex, cipher_suite_provider: CSP) -> Self1936 fn new(tree: &'a TreeKemPublic, sender: LeafIndex, cipher_suite_provider: CSP) -> Self {
1937 Self {
1938 tree,
1939 sender,
1940 cache: make_proposal_cache(),
1941 additional_proposals: Vec::new(),
1942 identity_provider: BasicWithCustomProvider::new(BasicIdentityProvider::new()),
1943 user_rules: pass_through_rules(),
1944 psk_storage: AlwaysFoundPskStorage,
1945 cipher_suite_provider,
1946 }
1947 }
1948 }
1949
1950 impl<'a, C, F, P, CSP> CommitSender<'a, C, F, P, CSP>
1951 where
1952 C: IdentityProvider,
1953 F: MlsRules,
1954 P: PreSharedKeyStorage,
1955 CSP: CipherSuiteProvider,
1956 {
1957 #[cfg(feature = "by_ref_proposal")]
with_identity_provider<V>(self, identity_provider: V) -> CommitSender<'a, V, F, P, CSP> where V: IdentityProvider,1958 fn with_identity_provider<V>(self, identity_provider: V) -> CommitSender<'a, V, F, P, CSP>
1959 where
1960 V: IdentityProvider,
1961 {
1962 CommitSender {
1963 identity_provider,
1964 cipher_suite_provider: self.cipher_suite_provider,
1965 tree: self.tree,
1966 sender: self.sender,
1967 cache: self.cache,
1968 additional_proposals: self.additional_proposals,
1969 user_rules: self.user_rules,
1970 psk_storage: self.psk_storage,
1971 }
1972 }
1973
cache<S>(mut self, r: ProposalRef, p: Proposal, proposer: S) -> Self where S: Into<Sender>,1974 fn cache<S>(mut self, r: ProposalRef, p: Proposal, proposer: S) -> Self
1975 where
1976 S: Into<Sender>,
1977 {
1978 self.cache.insert(r, p, proposer.into());
1979 self
1980 }
1981
with_additional<I>(mut self, proposals: I) -> Self where I: IntoIterator<Item = Proposal>,1982 fn with_additional<I>(mut self, proposals: I) -> Self
1983 where
1984 I: IntoIterator<Item = Proposal>,
1985 {
1986 self.additional_proposals.extend(proposals);
1987 self
1988 }
1989
with_user_rules<G>(self, f: G) -> CommitSender<'a, C, G, P, CSP> where G: MlsRules,1990 fn with_user_rules<G>(self, f: G) -> CommitSender<'a, C, G, P, CSP>
1991 where
1992 G: MlsRules,
1993 {
1994 CommitSender {
1995 tree: self.tree,
1996 sender: self.sender,
1997 cache: self.cache,
1998 additional_proposals: self.additional_proposals,
1999 identity_provider: self.identity_provider,
2000 user_rules: f,
2001 psk_storage: self.psk_storage,
2002 cipher_suite_provider: self.cipher_suite_provider,
2003 }
2004 }
2005
with_psk_storage<V>(self, v: V) -> CommitSender<'a, C, F, V, CSP> where V: PreSharedKeyStorage,2006 fn with_psk_storage<V>(self, v: V) -> CommitSender<'a, C, F, V, CSP>
2007 where
2008 V: PreSharedKeyStorage,
2009 {
2010 CommitSender {
2011 tree: self.tree,
2012 sender: self.sender,
2013 cache: self.cache,
2014 additional_proposals: self.additional_proposals,
2015 identity_provider: self.identity_provider,
2016 user_rules: self.user_rules,
2017 psk_storage: v,
2018 cipher_suite_provider: self.cipher_suite_provider,
2019 }
2020 }
2021
2022 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
send(&self) -> Result<(Vec<ProposalOrRef>, ProvisionalState), MlsError>2023 async fn send(&self) -> Result<(Vec<ProposalOrRef>, ProvisionalState), MlsError> {
2024 let state = self
2025 .cache
2026 .prepare_commit_default(
2027 Sender::Member(*self.sender),
2028 self.additional_proposals.clone(),
2029 &get_test_group_context(1, TEST_CIPHER_SUITE).await,
2030 &self.identity_provider,
2031 &self.cipher_suite_provider,
2032 self.tree,
2033 None,
2034 &self.psk_storage,
2035 &self.user_rules,
2036 )
2037 .await?;
2038
2039 let proposals = state.applied_proposals.clone().into_proposals_or_refs();
2040
2041 Ok((proposals, state))
2042 }
2043 }
2044
2045 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
key_package_with_invalid_signature() -> KeyPackage2046 async fn key_package_with_invalid_signature() -> KeyPackage {
2047 let mut kp = test_key_package(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "mallory").await;
2048 kp.signature.clear();
2049 kp
2050 }
2051
2052 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
key_package_with_public_key(key: crypto::HpkePublicKey) -> KeyPackage2053 async fn key_package_with_public_key(key: crypto::HpkePublicKey) -> KeyPackage {
2054 let cs = test_cipher_suite_provider(TEST_CIPHER_SUITE);
2055
2056 let (mut key_package, signer) =
2057 test_key_package_with_signer(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "test").await;
2058
2059 key_package.leaf_node.public_key = key;
2060
2061 key_package
2062 .leaf_node
2063 .sign(
2064 &cs,
2065 &signer,
2066 &LeafNodeSigningContext {
2067 group_id: None,
2068 leaf_index: None,
2069 },
2070 )
2071 .await
2072 .unwrap();
2073
2074 key_package.sign(&cs, &signer, &()).await.unwrap();
2075
2076 key_package
2077 }
2078
2079 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
receiving_add_with_invalid_key_package_fails()2080 async fn receiving_add_with_invalid_key_package_fails() {
2081 let (alice, tree) = new_tree("alice").await;
2082
2083 let res = CommitReceiver::new(
2084 &tree,
2085 alice,
2086 alice,
2087 test_cipher_suite_provider(TEST_CIPHER_SUITE),
2088 )
2089 .receive([Proposal::Add(Box::new(AddProposal {
2090 key_package: key_package_with_invalid_signature().await,
2091 }))])
2092 .await;
2093
2094 assert_matches!(res, Err(MlsError::InvalidSignature));
2095 }
2096
2097 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_additional_add_with_invalid_key_package_fails()2098 async fn sending_additional_add_with_invalid_key_package_fails() {
2099 let (alice, tree) = new_tree("alice").await;
2100
2101 let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
2102 .with_additional([Proposal::Add(Box::new(AddProposal {
2103 key_package: key_package_with_invalid_signature().await,
2104 }))])
2105 .send()
2106 .await;
2107
2108 assert_matches!(res, Err(MlsError::InvalidSignature));
2109 }
2110
2111 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_add_with_invalid_key_package_filters_it_out()2112 async fn sending_add_with_invalid_key_package_filters_it_out() {
2113 let (alice, tree) = new_tree("alice").await;
2114
2115 let proposal = Proposal::Add(Box::new(AddProposal {
2116 key_package: key_package_with_invalid_signature().await,
2117 }));
2118
2119 let proposal_info = make_proposal_info(&proposal, alice).await;
2120
2121 let processed_proposals =
2122 CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
2123 .cache(
2124 proposal_info.proposal_ref().unwrap().clone(),
2125 proposal.clone(),
2126 alice,
2127 )
2128 .send()
2129 .await
2130 .unwrap();
2131
2132 assert_eq!(processed_proposals.0, Vec::new());
2133
2134 #[cfg(feature = "state_update")]
2135 assert_eq!(processed_proposals.1.unused_proposals, vec![proposal_info]);
2136 }
2137
2138 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_add_with_hpke_key_of_another_member_fails()2139 async fn sending_add_with_hpke_key_of_another_member_fails() {
2140 let (alice, tree) = new_tree("alice").await;
2141
2142 let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
2143 .with_additional([Proposal::Add(Box::new(AddProposal {
2144 key_package: key_package_with_public_key(
2145 tree.get_leaf_node(alice).unwrap().public_key.clone(),
2146 )
2147 .await,
2148 }))])
2149 .send()
2150 .await;
2151
2152 assert_matches!(res, Err(MlsError::DuplicateLeafData(_)));
2153 }
2154
2155 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_add_with_hpke_key_of_another_member_filters_it_out()2156 async fn sending_add_with_hpke_key_of_another_member_filters_it_out() {
2157 let (alice, tree) = new_tree("alice").await;
2158
2159 let proposal = Proposal::Add(Box::new(AddProposal {
2160 key_package: key_package_with_public_key(
2161 tree.get_leaf_node(alice).unwrap().public_key.clone(),
2162 )
2163 .await,
2164 }));
2165
2166 let proposal_info = make_proposal_info(&proposal, alice).await;
2167
2168 let processed_proposals =
2169 CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
2170 .cache(
2171 proposal_info.proposal_ref().unwrap().clone(),
2172 proposal.clone(),
2173 alice,
2174 )
2175 .send()
2176 .await
2177 .unwrap();
2178
2179 assert_eq!(processed_proposals.0, Vec::new());
2180
2181 #[cfg(feature = "state_update")]
2182 assert_eq!(processed_proposals.1.unused_proposals, vec![proposal_info]);
2183 }
2184
2185 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
receiving_update_with_invalid_leaf_node_fails()2186 async fn receiving_update_with_invalid_leaf_node_fails() {
2187 let (alice, mut tree) = new_tree("alice").await;
2188 let bob = add_member(&mut tree, "bob").await;
2189
2190 let proposal = Proposal::Update(UpdateProposal {
2191 leaf_node: get_basic_test_node(TEST_CIPHER_SUITE, "alice").await,
2192 });
2193
2194 let proposal_ref = make_proposal_ref(&proposal, bob).await;
2195
2196 let res = CommitReceiver::new(
2197 &tree,
2198 alice,
2199 bob,
2200 test_cipher_suite_provider(TEST_CIPHER_SUITE),
2201 )
2202 .cache(proposal_ref.clone(), proposal, bob)
2203 .receive([proposal_ref])
2204 .await;
2205
2206 assert_matches!(res, Err(MlsError::InvalidLeafNodeSource));
2207 }
2208
2209 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_update_with_invalid_leaf_node_filters_it_out()2210 async fn sending_update_with_invalid_leaf_node_filters_it_out() {
2211 let (alice, mut tree) = new_tree("alice").await;
2212 let bob = add_member(&mut tree, "bob").await;
2213
2214 let proposal = Proposal::Update(UpdateProposal {
2215 leaf_node: get_basic_test_node(TEST_CIPHER_SUITE, "alice").await,
2216 });
2217
2218 let proposal_info = make_proposal_info(&proposal, bob).await;
2219
2220 let processed_proposals =
2221 CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
2222 .cache(proposal_info.proposal_ref().unwrap().clone(), proposal, bob)
2223 .send()
2224 .await
2225 .unwrap();
2226
2227 assert_eq!(processed_proposals.0, Vec::new());
2228
2229 #[cfg(feature = "state_update")]
2230 assert_eq!(processed_proposals.1.unused_proposals, vec![proposal_info]);
2231 }
2232
2233 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
receiving_remove_with_invalid_index_fails()2234 async fn receiving_remove_with_invalid_index_fails() {
2235 let (alice, tree) = new_tree("alice").await;
2236
2237 let res = CommitReceiver::new(
2238 &tree,
2239 alice,
2240 alice,
2241 test_cipher_suite_provider(TEST_CIPHER_SUITE),
2242 )
2243 .receive([Proposal::Remove(RemoveProposal {
2244 to_remove: LeafIndex(10),
2245 })])
2246 .await;
2247
2248 assert_matches!(res, Err(MlsError::InvalidNodeIndex(20)));
2249 }
2250
2251 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_additional_remove_with_invalid_index_fails()2252 async fn sending_additional_remove_with_invalid_index_fails() {
2253 let (alice, tree) = new_tree("alice").await;
2254
2255 let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
2256 .with_additional([Proposal::Remove(RemoveProposal {
2257 to_remove: LeafIndex(10),
2258 })])
2259 .send()
2260 .await;
2261
2262 assert_matches!(res, Err(MlsError::InvalidNodeIndex(20)));
2263 }
2264
2265 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_remove_with_invalid_index_filters_it_out()2266 async fn sending_remove_with_invalid_index_filters_it_out() {
2267 let (alice, tree) = new_tree("alice").await;
2268
2269 let proposal = Proposal::Remove(RemoveProposal {
2270 to_remove: LeafIndex(10),
2271 });
2272
2273 let proposal_info = make_proposal_info(&proposal, alice).await;
2274
2275 let processed_proposals =
2276 CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
2277 .cache(
2278 proposal_info.proposal_ref().unwrap().clone(),
2279 proposal.clone(),
2280 alice,
2281 )
2282 .send()
2283 .await
2284 .unwrap();
2285
2286 assert_eq!(processed_proposals.0, Vec::new());
2287
2288 #[cfg(feature = "state_update")]
2289 assert_eq!(processed_proposals.1.unused_proposals, vec![proposal_info]);
2290 }
2291
2292 #[cfg(feature = "psk")]
make_external_psk(id: &[u8], nonce: PskNonce) -> PreSharedKeyProposal2293 fn make_external_psk(id: &[u8], nonce: PskNonce) -> PreSharedKeyProposal {
2294 PreSharedKeyProposal {
2295 psk: PreSharedKeyID {
2296 key_id: JustPreSharedKeyID::External(ExternalPskId::new(id.to_vec())),
2297 psk_nonce: nonce,
2298 },
2299 }
2300 }
2301
2302 #[cfg(feature = "psk")]
new_external_psk(id: &[u8]) -> PreSharedKeyProposal2303 fn new_external_psk(id: &[u8]) -> PreSharedKeyProposal {
2304 make_external_psk(
2305 id,
2306 PskNonce::random(&test_cipher_suite_provider(TEST_CIPHER_SUITE)).unwrap(),
2307 )
2308 }
2309
2310 #[cfg(feature = "psk")]
2311 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
receiving_psk_with_invalid_nonce_fails()2312 async fn receiving_psk_with_invalid_nonce_fails() {
2313 let invalid_nonce = PskNonce(vec![0, 1, 2]);
2314 let (alice, tree) = new_tree("alice").await;
2315
2316 let res = CommitReceiver::new(
2317 &tree,
2318 alice,
2319 alice,
2320 test_cipher_suite_provider(TEST_CIPHER_SUITE),
2321 )
2322 .receive([Proposal::Psk(make_external_psk(
2323 b"foo",
2324 invalid_nonce.clone(),
2325 ))])
2326 .await;
2327
2328 assert_matches!(res, Err(MlsError::InvalidPskNonceLength,));
2329 }
2330
2331 #[cfg(feature = "psk")]
2332 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_additional_psk_with_invalid_nonce_fails()2333 async fn sending_additional_psk_with_invalid_nonce_fails() {
2334 let invalid_nonce = PskNonce(vec![0, 1, 2]);
2335 let (alice, tree) = new_tree("alice").await;
2336
2337 let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
2338 .with_additional([Proposal::Psk(make_external_psk(
2339 b"foo",
2340 invalid_nonce.clone(),
2341 ))])
2342 .send()
2343 .await;
2344
2345 assert_matches!(res, Err(MlsError::InvalidPskNonceLength));
2346 }
2347
2348 #[cfg(feature = "psk")]
2349 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_psk_with_invalid_nonce_filters_it_out()2350 async fn sending_psk_with_invalid_nonce_filters_it_out() {
2351 let invalid_nonce = PskNonce(vec![0, 1, 2]);
2352 let (alice, tree) = new_tree("alice").await;
2353 let proposal = Proposal::Psk(make_external_psk(b"foo", invalid_nonce));
2354
2355 let proposal_info = make_proposal_info(&proposal, alice).await;
2356
2357 let processed_proposals =
2358 CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
2359 .cache(
2360 proposal_info.proposal_ref().unwrap().clone(),
2361 proposal.clone(),
2362 alice,
2363 )
2364 .send()
2365 .await
2366 .unwrap();
2367
2368 assert_eq!(processed_proposals.0, Vec::new());
2369
2370 #[cfg(feature = "state_update")]
2371 assert_eq!(processed_proposals.1.unused_proposals, vec![proposal_info]);
2372 }
2373
2374 #[cfg(feature = "psk")]
make_resumption_psk(usage: ResumptionPSKUsage) -> PreSharedKeyProposal2375 fn make_resumption_psk(usage: ResumptionPSKUsage) -> PreSharedKeyProposal {
2376 PreSharedKeyProposal {
2377 psk: PreSharedKeyID {
2378 key_id: JustPreSharedKeyID::Resumption(ResumptionPsk {
2379 usage,
2380 psk_group_id: PskGroupId(TEST_GROUP.to_vec()),
2381 psk_epoch: 1,
2382 }),
2383 psk_nonce: PskNonce::random(&test_cipher_suite_provider(TEST_CIPHER_SUITE))
2384 .unwrap(),
2385 },
2386 }
2387 }
2388
2389 #[cfg(feature = "psk")]
2390 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
receiving_resumption_psk_with_bad_usage_fails(usage: ResumptionPSKUsage)2391 async fn receiving_resumption_psk_with_bad_usage_fails(usage: ResumptionPSKUsage) {
2392 let (alice, tree) = new_tree("alice").await;
2393
2394 let res = CommitReceiver::new(
2395 &tree,
2396 alice,
2397 alice,
2398 test_cipher_suite_provider(TEST_CIPHER_SUITE),
2399 )
2400 .receive([Proposal::Psk(make_resumption_psk(usage))])
2401 .await;
2402
2403 assert_matches!(res, Err(MlsError::InvalidTypeOrUsageInPreSharedKeyProposal));
2404 }
2405
2406 #[cfg(feature = "psk")]
2407 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
sending_additional_resumption_psk_with_bad_usage_fails(usage: ResumptionPSKUsage)2408 async fn sending_additional_resumption_psk_with_bad_usage_fails(usage: ResumptionPSKUsage) {
2409 let (alice, tree) = new_tree("alice").await;
2410
2411 let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
2412 .with_additional([Proposal::Psk(make_resumption_psk(usage))])
2413 .send()
2414 .await;
2415
2416 assert_matches!(res, Err(MlsError::InvalidTypeOrUsageInPreSharedKeyProposal));
2417 }
2418
2419 #[cfg(feature = "psk")]
2420 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
sending_resumption_psk_with_bad_usage_filters_it_out(usage: ResumptionPSKUsage)2421 async fn sending_resumption_psk_with_bad_usage_filters_it_out(usage: ResumptionPSKUsage) {
2422 let (alice, tree) = new_tree("alice").await;
2423 let proposal = Proposal::Psk(make_resumption_psk(usage));
2424 let proposal_info = make_proposal_info(&proposal, alice).await;
2425
2426 let processed_proposals =
2427 CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
2428 .cache(
2429 proposal_info.proposal_ref().unwrap().clone(),
2430 proposal.clone(),
2431 alice,
2432 )
2433 .send()
2434 .await
2435 .unwrap();
2436
2437 assert_eq!(processed_proposals.0, Vec::new());
2438
2439 #[cfg(feature = "state_update")]
2440 assert_eq!(processed_proposals.1.unused_proposals, vec![proposal_info]);
2441 }
2442
2443 #[cfg(feature = "psk")]
2444 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
receiving_resumption_psk_with_reinit_usage_fails()2445 async fn receiving_resumption_psk_with_reinit_usage_fails() {
2446 receiving_resumption_psk_with_bad_usage_fails(ResumptionPSKUsage::Reinit).await;
2447 }
2448
2449 #[cfg(feature = "psk")]
2450 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_additional_resumption_psk_with_reinit_usage_fails()2451 async fn sending_additional_resumption_psk_with_reinit_usage_fails() {
2452 sending_additional_resumption_psk_with_bad_usage_fails(ResumptionPSKUsage::Reinit).await;
2453 }
2454
2455 #[cfg(feature = "psk")]
2456 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_resumption_psk_with_reinit_usage_filters_it_out()2457 async fn sending_resumption_psk_with_reinit_usage_filters_it_out() {
2458 sending_resumption_psk_with_bad_usage_filters_it_out(ResumptionPSKUsage::Reinit).await;
2459 }
2460
2461 #[cfg(feature = "psk")]
2462 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
receiving_resumption_psk_with_branch_usage_fails()2463 async fn receiving_resumption_psk_with_branch_usage_fails() {
2464 receiving_resumption_psk_with_bad_usage_fails(ResumptionPSKUsage::Branch).await;
2465 }
2466
2467 #[cfg(feature = "psk")]
2468 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_additional_resumption_psk_with_branch_usage_fails()2469 async fn sending_additional_resumption_psk_with_branch_usage_fails() {
2470 sending_additional_resumption_psk_with_bad_usage_fails(ResumptionPSKUsage::Branch).await;
2471 }
2472
2473 #[cfg(feature = "psk")]
2474 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_resumption_psk_with_branch_usage_filters_it_out()2475 async fn sending_resumption_psk_with_branch_usage_filters_it_out() {
2476 sending_resumption_psk_with_bad_usage_filters_it_out(ResumptionPSKUsage::Branch).await;
2477 }
2478
make_reinit(version: ProtocolVersion) -> ReInitProposal2479 fn make_reinit(version: ProtocolVersion) -> ReInitProposal {
2480 ReInitProposal {
2481 group_id: TEST_GROUP.to_vec(),
2482 version,
2483 cipher_suite: TEST_CIPHER_SUITE,
2484 extensions: ExtensionList::new(),
2485 }
2486 }
2487
2488 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
receiving_reinit_downgrading_version_fails()2489 async fn receiving_reinit_downgrading_version_fails() {
2490 let smaller_protocol_version = ProtocolVersion::from(0);
2491 let (alice, tree) = new_tree("alice").await;
2492
2493 let res = CommitReceiver::new(
2494 &tree,
2495 alice,
2496 alice,
2497 test_cipher_suite_provider(TEST_CIPHER_SUITE),
2498 )
2499 .receive([Proposal::ReInit(make_reinit(smaller_protocol_version))])
2500 .await;
2501
2502 assert_matches!(res, Err(MlsError::InvalidProtocolVersionInReInit));
2503 }
2504
2505 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_additional_reinit_downgrading_version_fails()2506 async fn sending_additional_reinit_downgrading_version_fails() {
2507 let smaller_protocol_version = ProtocolVersion::from(0);
2508 let (alice, tree) = new_tree("alice").await;
2509
2510 let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
2511 .with_additional([Proposal::ReInit(make_reinit(smaller_protocol_version))])
2512 .send()
2513 .await;
2514
2515 assert_matches!(res, Err(MlsError::InvalidProtocolVersionInReInit));
2516 }
2517
2518 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_reinit_downgrading_version_filters_it_out()2519 async fn sending_reinit_downgrading_version_filters_it_out() {
2520 let smaller_protocol_version = ProtocolVersion::from(0);
2521 let (alice, tree) = new_tree("alice").await;
2522 let proposal = Proposal::ReInit(make_reinit(smaller_protocol_version));
2523 let proposal_info = make_proposal_info(&proposal, alice).await;
2524
2525 let processed_proposals =
2526 CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
2527 .cache(
2528 proposal_info.proposal_ref().unwrap().clone(),
2529 proposal.clone(),
2530 alice,
2531 )
2532 .send()
2533 .await
2534 .unwrap();
2535
2536 assert_eq!(processed_proposals.0, Vec::new());
2537
2538 #[cfg(feature = "state_update")]
2539 assert_eq!(processed_proposals.1.unused_proposals, vec![proposal_info]);
2540 }
2541
2542 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
receiving_update_for_committer_fails()2543 async fn receiving_update_for_committer_fails() {
2544 let (alice, tree) = new_tree("alice").await;
2545 let update = Proposal::Update(make_update_proposal("alice").await);
2546 let update_ref = make_proposal_ref(&update, alice).await;
2547
2548 let res = CommitReceiver::new(
2549 &tree,
2550 alice,
2551 alice,
2552 test_cipher_suite_provider(TEST_CIPHER_SUITE),
2553 )
2554 .cache(update_ref.clone(), update, alice)
2555 .receive([update_ref])
2556 .await;
2557
2558 assert_matches!(res, Err(MlsError::InvalidCommitSelfUpdate));
2559 }
2560
2561 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_additional_update_for_committer_fails()2562 async fn sending_additional_update_for_committer_fails() {
2563 let (alice, tree) = new_tree("alice").await;
2564
2565 let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
2566 .with_additional([Proposal::Update(make_update_proposal("alice").await)])
2567 .send()
2568 .await;
2569
2570 assert_matches!(res, Err(MlsError::InvalidProposalTypeForSender));
2571 }
2572
2573 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_update_for_committer_filters_it_out()2574 async fn sending_update_for_committer_filters_it_out() {
2575 let (alice, tree) = new_tree("alice").await;
2576 let proposal = Proposal::Update(make_update_proposal("alice").await);
2577 let proposal_info = make_proposal_info(&proposal, alice).await;
2578
2579 let processed_proposals =
2580 CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
2581 .cache(
2582 proposal_info.proposal_ref().unwrap().clone(),
2583 proposal.clone(),
2584 alice,
2585 )
2586 .send()
2587 .await
2588 .unwrap();
2589
2590 assert_eq!(processed_proposals.0, Vec::new());
2591
2592 #[cfg(feature = "state_update")]
2593 assert_eq!(processed_proposals.1.unused_proposals, vec![proposal_info]);
2594 }
2595
2596 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
receiving_remove_for_committer_fails()2597 async fn receiving_remove_for_committer_fails() {
2598 let (alice, tree) = new_tree("alice").await;
2599
2600 let res = CommitReceiver::new(
2601 &tree,
2602 alice,
2603 alice,
2604 test_cipher_suite_provider(TEST_CIPHER_SUITE),
2605 )
2606 .receive([Proposal::Remove(RemoveProposal { to_remove: alice })])
2607 .await;
2608
2609 assert_matches!(res, Err(MlsError::CommitterSelfRemoval));
2610 }
2611
2612 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_additional_remove_for_committer_fails()2613 async fn sending_additional_remove_for_committer_fails() {
2614 let (alice, tree) = new_tree("alice").await;
2615
2616 let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
2617 .with_additional([Proposal::Remove(RemoveProposal { to_remove: alice })])
2618 .send()
2619 .await;
2620
2621 assert_matches!(res, Err(MlsError::CommitterSelfRemoval));
2622 }
2623
2624 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_remove_for_committer_filters_it_out()2625 async fn sending_remove_for_committer_filters_it_out() {
2626 let (alice, tree) = new_tree("alice").await;
2627 let proposal = Proposal::Remove(RemoveProposal { to_remove: alice });
2628 let proposal_info = make_proposal_info(&proposal, alice).await;
2629
2630 let processed_proposals =
2631 CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
2632 .cache(
2633 proposal_info.proposal_ref().unwrap().clone(),
2634 proposal.clone(),
2635 alice,
2636 )
2637 .send()
2638 .await
2639 .unwrap();
2640
2641 assert_eq!(processed_proposals.0, Vec::new());
2642
2643 #[cfg(feature = "state_update")]
2644 assert_eq!(processed_proposals.1.unused_proposals, vec![proposal_info]);
2645 }
2646
2647 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
receiving_update_and_remove_for_same_leaf_fails()2648 async fn receiving_update_and_remove_for_same_leaf_fails() {
2649 let (alice, mut tree) = new_tree("alice").await;
2650 let bob = add_member(&mut tree, "bob").await;
2651
2652 let update = Proposal::Update(make_update_proposal("bob").await);
2653 let update_ref = make_proposal_ref(&update, bob).await;
2654
2655 let remove = Proposal::Remove(RemoveProposal { to_remove: bob });
2656 let remove_ref = make_proposal_ref(&remove, bob).await;
2657
2658 let res = CommitReceiver::new(
2659 &tree,
2660 alice,
2661 alice,
2662 test_cipher_suite_provider(TEST_CIPHER_SUITE),
2663 )
2664 .cache(update_ref.clone(), update, bob)
2665 .cache(remove_ref.clone(), remove, bob)
2666 .receive([update_ref, remove_ref])
2667 .await;
2668
2669 assert_matches!(res, Err(MlsError::UpdatingNonExistingMember));
2670 }
2671
2672 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_update_and_remove_for_same_leaf_filters_update_out()2673 async fn sending_update_and_remove_for_same_leaf_filters_update_out() {
2674 let (alice, mut tree) = new_tree("alice").await;
2675 let bob = add_member(&mut tree, "bob").await;
2676
2677 let update = Proposal::Update(make_update_proposal("bob").await);
2678 let update_info = make_proposal_info(&update, alice).await;
2679
2680 let remove = Proposal::Remove(RemoveProposal { to_remove: bob });
2681 let remove_ref = make_proposal_ref(&remove, alice).await;
2682
2683 let processed_proposals =
2684 CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
2685 .cache(
2686 update_info.proposal_ref().unwrap().clone(),
2687 update.clone(),
2688 alice,
2689 )
2690 .cache(remove_ref.clone(), remove, alice)
2691 .send()
2692 .await
2693 .unwrap();
2694
2695 assert_eq!(processed_proposals.0, vec![remove_ref.into()]);
2696
2697 #[cfg(feature = "state_update")]
2698 assert_eq!(processed_proposals.1.unused_proposals, vec![update_info]);
2699 }
2700
2701 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
make_add_proposal() -> Box<AddProposal>2702 async fn make_add_proposal() -> Box<AddProposal> {
2703 Box::new(AddProposal {
2704 key_package: test_key_package(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "frank").await,
2705 })
2706 }
2707
2708 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
receiving_add_proposals_for_same_client_fails()2709 async fn receiving_add_proposals_for_same_client_fails() {
2710 let (alice, tree) = new_tree("alice").await;
2711
2712 let res = CommitReceiver::new(
2713 &tree,
2714 alice,
2715 alice,
2716 test_cipher_suite_provider(TEST_CIPHER_SUITE),
2717 )
2718 .receive([
2719 Proposal::Add(make_add_proposal().await),
2720 Proposal::Add(make_add_proposal().await),
2721 ])
2722 .await;
2723
2724 assert_matches!(res, Err(MlsError::DuplicateLeafData(1)));
2725 }
2726
2727 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_additional_add_proposals_for_same_client_fails()2728 async fn sending_additional_add_proposals_for_same_client_fails() {
2729 let (alice, tree) = new_tree("alice").await;
2730
2731 let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
2732 .with_additional([
2733 Proposal::Add(make_add_proposal().await),
2734 Proposal::Add(make_add_proposal().await),
2735 ])
2736 .send()
2737 .await;
2738
2739 assert_matches!(res, Err(MlsError::DuplicateLeafData(1)));
2740 }
2741
2742 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_add_proposals_for_same_client_keeps_only_one()2743 async fn sending_add_proposals_for_same_client_keeps_only_one() {
2744 let (alice, tree) = new_tree("alice").await;
2745
2746 let add_one = Proposal::Add(make_add_proposal().await);
2747 let add_two = Proposal::Add(make_add_proposal().await);
2748 let add_ref_one = make_proposal_ref(&add_one, alice).await;
2749 let add_ref_two = make_proposal_ref(&add_two, alice).await;
2750
2751 let processed_proposals =
2752 CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
2753 .cache(add_ref_one.clone(), add_one.clone(), alice)
2754 .cache(add_ref_two.clone(), add_two.clone(), alice)
2755 .send()
2756 .await
2757 .unwrap();
2758
2759 let committed_add_ref = match &*processed_proposals.0 {
2760 [ProposalOrRef::Reference(add_ref)] => add_ref,
2761 _ => panic!("committed proposals list does not contain exactly one reference"),
2762 };
2763
2764 let add_refs = [add_ref_one, add_ref_two];
2765 assert!(add_refs.contains(committed_add_ref));
2766
2767 #[cfg(feature = "state_update")]
2768 assert_matches!(
2769 &*processed_proposals.1.unused_proposals,
2770 [rejected_add_info] if committed_add_ref != rejected_add_info.proposal_ref().unwrap() && add_refs.contains(rejected_add_info.proposal_ref().unwrap())
2771 );
2772 }
2773
2774 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
receiving_update_for_different_identity_fails()2775 async fn receiving_update_for_different_identity_fails() {
2776 let (alice, mut tree) = new_tree("alice").await;
2777 let bob = add_member(&mut tree, "bob").await;
2778
2779 let update = Proposal::Update(make_update_proposal_custom("carol", 1).await);
2780 let update_ref = make_proposal_ref(&update, bob).await;
2781
2782 let res = CommitReceiver::new(
2783 &tree,
2784 alice,
2785 alice,
2786 test_cipher_suite_provider(TEST_CIPHER_SUITE),
2787 )
2788 .cache(update_ref.clone(), update, bob)
2789 .receive([update_ref])
2790 .await;
2791
2792 assert_matches!(res, Err(MlsError::InvalidSuccessor));
2793 }
2794
2795 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_update_for_different_identity_filters_it_out()2796 async fn sending_update_for_different_identity_filters_it_out() {
2797 let (alice, mut tree) = new_tree("alice").await;
2798 let bob = add_member(&mut tree, "bob").await;
2799
2800 let update = Proposal::Update(make_update_proposal("carol").await);
2801 let update_info = make_proposal_info(&update, bob).await;
2802
2803 let processed_proposals =
2804 CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
2805 .cache(update_info.proposal_ref().unwrap().clone(), update, bob)
2806 .send()
2807 .await
2808 .unwrap();
2809
2810 assert_eq!(processed_proposals.0, Vec::new());
2811
2812 // Bob proposed the update, so it is not listed as rejected when Alice commits it because
2813 // she didn't propose it.
2814 #[cfg(feature = "state_update")]
2815 assert_eq!(processed_proposals.1.unused_proposals, vec![update_info]);
2816 }
2817
2818 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
receiving_add_for_same_client_as_existing_member_fails()2819 async fn receiving_add_for_same_client_as_existing_member_fails() {
2820 let (alice, public_tree) = new_tree("alice").await;
2821 let add = Proposal::Add(make_add_proposal().await);
2822
2823 let ProvisionalState { public_tree, .. } = CommitReceiver::new(
2824 &public_tree,
2825 alice,
2826 alice,
2827 test_cipher_suite_provider(TEST_CIPHER_SUITE),
2828 )
2829 .receive([add.clone()])
2830 .await
2831 .unwrap();
2832
2833 let res = CommitReceiver::new(
2834 &public_tree,
2835 alice,
2836 alice,
2837 test_cipher_suite_provider(TEST_CIPHER_SUITE),
2838 )
2839 .receive([add])
2840 .await;
2841
2842 assert_matches!(res, Err(MlsError::DuplicateLeafData(1)));
2843 }
2844
2845 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_additional_add_for_same_client_as_existing_member_fails()2846 async fn sending_additional_add_for_same_client_as_existing_member_fails() {
2847 let (alice, public_tree) = new_tree("alice").await;
2848 let add = Proposal::Add(make_add_proposal().await);
2849
2850 let ProvisionalState { public_tree, .. } = CommitReceiver::new(
2851 &public_tree,
2852 alice,
2853 alice,
2854 test_cipher_suite_provider(TEST_CIPHER_SUITE),
2855 )
2856 .receive([add.clone()])
2857 .await
2858 .unwrap();
2859
2860 let res = CommitSender::new(
2861 &public_tree,
2862 alice,
2863 test_cipher_suite_provider(TEST_CIPHER_SUITE),
2864 )
2865 .with_additional([add])
2866 .send()
2867 .await;
2868
2869 assert_matches!(res, Err(MlsError::DuplicateLeafData(1)));
2870 }
2871
2872 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_add_for_same_client_as_existing_member_filters_it_out()2873 async fn sending_add_for_same_client_as_existing_member_filters_it_out() {
2874 let (alice, public_tree) = new_tree("alice").await;
2875 let add = Proposal::Add(make_add_proposal().await);
2876
2877 let ProvisionalState { public_tree, .. } = CommitReceiver::new(
2878 &public_tree,
2879 alice,
2880 alice,
2881 test_cipher_suite_provider(TEST_CIPHER_SUITE),
2882 )
2883 .receive([add.clone()])
2884 .await
2885 .unwrap();
2886
2887 let proposal_info = make_proposal_info(&add, alice).await;
2888
2889 let processed_proposals = CommitSender::new(
2890 &public_tree,
2891 alice,
2892 test_cipher_suite_provider(TEST_CIPHER_SUITE),
2893 )
2894 .cache(
2895 proposal_info.proposal_ref().unwrap().clone(),
2896 add.clone(),
2897 alice,
2898 )
2899 .send()
2900 .await
2901 .unwrap();
2902
2903 assert_eq!(processed_proposals.0, Vec::new());
2904
2905 #[cfg(feature = "state_update")]
2906 assert_eq!(processed_proposals.1.unused_proposals, vec![proposal_info]);
2907 }
2908
2909 #[cfg(feature = "psk")]
2910 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
receiving_psk_proposals_with_same_psk_id_fails()2911 async fn receiving_psk_proposals_with_same_psk_id_fails() {
2912 let (alice, tree) = new_tree("alice").await;
2913 let psk_proposal = Proposal::Psk(new_external_psk(b"foo"));
2914
2915 let res = CommitReceiver::new(
2916 &tree,
2917 alice,
2918 alice,
2919 test_cipher_suite_provider(TEST_CIPHER_SUITE),
2920 )
2921 .receive([psk_proposal.clone(), psk_proposal])
2922 .await;
2923
2924 assert_matches!(res, Err(MlsError::DuplicatePskIds));
2925 }
2926
2927 #[cfg(feature = "psk")]
2928 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_additional_psk_proposals_with_same_psk_id_fails()2929 async fn sending_additional_psk_proposals_with_same_psk_id_fails() {
2930 let (alice, tree) = new_tree("alice").await;
2931 let psk_proposal = Proposal::Psk(new_external_psk(b"foo"));
2932
2933 let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
2934 .with_additional([psk_proposal.clone(), psk_proposal])
2935 .send()
2936 .await;
2937
2938 assert_matches!(res, Err(MlsError::DuplicatePskIds));
2939 }
2940
2941 #[cfg(feature = "psk")]
2942 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_psk_proposals_with_same_psk_id_keeps_only_one()2943 async fn sending_psk_proposals_with_same_psk_id_keeps_only_one() {
2944 let (alice, mut tree) = new_tree("alice").await;
2945 let bob = add_member(&mut tree, "bob").await;
2946
2947 let proposal = Proposal::Psk(new_external_psk(b"foo"));
2948
2949 let proposal_info = [
2950 make_proposal_info(&proposal, alice).await,
2951 make_proposal_info(&proposal, bob).await,
2952 ];
2953
2954 let processed_proposals =
2955 CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
2956 .cache(
2957 proposal_info[0].proposal_ref().unwrap().clone(),
2958 proposal.clone(),
2959 alice,
2960 )
2961 .cache(
2962 proposal_info[1].proposal_ref().unwrap().clone(),
2963 proposal,
2964 bob,
2965 )
2966 .send()
2967 .await
2968 .unwrap();
2969
2970 let committed_info = match processed_proposals
2971 .1
2972 .applied_proposals
2973 .clone()
2974 .into_proposals()
2975 .collect_vec()
2976 .as_slice()
2977 {
2978 [r] => r.clone(),
2979 _ => panic!("Expected single proposal reference in {processed_proposals:?}"),
2980 };
2981
2982 assert!(proposal_info.contains(&committed_info));
2983
2984 #[cfg(feature = "state_update")]
2985 match &*processed_proposals.1.unused_proposals {
2986 [r] => {
2987 assert_ne!(*r, committed_info);
2988 assert!(proposal_info.contains(r));
2989 }
2990 _ => panic!(
2991 "Expected one proposal reference in {:?}",
2992 processed_proposals.1.unused_proposals
2993 ),
2994 }
2995 }
2996
2997 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
receiving_multiple_group_context_extensions_fails()2998 async fn receiving_multiple_group_context_extensions_fails() {
2999 let (alice, tree) = new_tree("alice").await;
3000
3001 let res = CommitReceiver::new(
3002 &tree,
3003 alice,
3004 alice,
3005 test_cipher_suite_provider(TEST_CIPHER_SUITE),
3006 )
3007 .receive([
3008 Proposal::GroupContextExtensions(ExtensionList::new()),
3009 Proposal::GroupContextExtensions(ExtensionList::new()),
3010 ])
3011 .await;
3012
3013 assert_matches!(
3014 res,
3015 Err(MlsError::MoreThanOneGroupContextExtensionsProposal)
3016 );
3017 }
3018
3019 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_multiple_additional_group_context_extensions_fails()3020 async fn sending_multiple_additional_group_context_extensions_fails() {
3021 let (alice, tree) = new_tree("alice").await;
3022
3023 let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
3024 .with_additional([
3025 Proposal::GroupContextExtensions(ExtensionList::new()),
3026 Proposal::GroupContextExtensions(ExtensionList::new()),
3027 ])
3028 .send()
3029 .await;
3030
3031 assert_matches!(
3032 res,
3033 Err(MlsError::MoreThanOneGroupContextExtensionsProposal)
3034 );
3035 }
3036
make_extension_list(foo: u8) -> ExtensionList3037 fn make_extension_list(foo: u8) -> ExtensionList {
3038 vec![TestExtension { foo }.into_extension().unwrap()].into()
3039 }
3040
3041 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_multiple_group_context_extensions_keeps_only_one()3042 async fn sending_multiple_group_context_extensions_keeps_only_one() {
3043 let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
3044
3045 let (alice, tree) = {
3046 let (signing_identity, signature_key) =
3047 get_test_signing_identity(TEST_CIPHER_SUITE, b"alice").await;
3048
3049 let properties = ConfigProperties {
3050 capabilities: Capabilities {
3051 extensions: vec![42.into()],
3052 ..Capabilities::default()
3053 },
3054 extensions: Default::default(),
3055 };
3056
3057 let (leaf, secret) = LeafNode::generate(
3058 &cipher_suite_provider,
3059 properties,
3060 signing_identity,
3061 &signature_key,
3062 Lifetime::years(1).unwrap(),
3063 )
3064 .await
3065 .unwrap();
3066
3067 let (pub_tree, priv_tree) =
3068 TreeKemPublic::derive(leaf, secret, &BasicIdentityProvider, &Default::default())
3069 .await
3070 .unwrap();
3071
3072 (priv_tree.self_index, pub_tree)
3073 };
3074
3075 let proposals = [
3076 Proposal::GroupContextExtensions(make_extension_list(0)),
3077 Proposal::GroupContextExtensions(make_extension_list(1)),
3078 ];
3079
3080 let gce_info = [
3081 make_proposal_info(&proposals[0], alice).await,
3082 make_proposal_info(&proposals[1], alice).await,
3083 ];
3084
3085 let processed_proposals =
3086 CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
3087 .cache(
3088 gce_info[0].proposal_ref().unwrap().clone(),
3089 proposals[0].clone(),
3090 alice,
3091 )
3092 .cache(
3093 gce_info[1].proposal_ref().unwrap().clone(),
3094 proposals[1].clone(),
3095 alice,
3096 )
3097 .send()
3098 .await
3099 .unwrap();
3100
3101 let committed_gce_info = match processed_proposals
3102 .1
3103 .applied_proposals
3104 .clone()
3105 .into_proposals()
3106 .collect_vec()
3107 .as_slice()
3108 {
3109 [gce_info] => gce_info.clone(),
3110 _ => panic!("committed proposals list does not contain exactly one reference"),
3111 };
3112
3113 assert!(gce_info.contains(&committed_gce_info));
3114
3115 #[cfg(feature = "state_update")]
3116 assert_matches!(
3117 &*processed_proposals.1.unused_proposals,
3118 [rejected_gce_info] if committed_gce_info != *rejected_gce_info && gce_info.contains(rejected_gce_info)
3119 );
3120 }
3121
3122 #[cfg(feature = "by_ref_proposal")]
3123 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
make_external_senders_extension() -> ExtensionList3124 async fn make_external_senders_extension() -> ExtensionList {
3125 let identity = get_test_signing_identity(TEST_CIPHER_SUITE, b"alice")
3126 .await
3127 .0;
3128
3129 vec![ExternalSendersExt::new(vec![identity])
3130 .into_extension()
3131 .unwrap()]
3132 .into()
3133 }
3134
3135 #[cfg(feature = "by_ref_proposal")]
3136 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
receiving_invalid_external_senders_extension_fails()3137 async fn receiving_invalid_external_senders_extension_fails() {
3138 let (alice, tree) = new_tree("alice").await;
3139
3140 let res = CommitReceiver::new(
3141 &tree,
3142 alice,
3143 alice,
3144 test_cipher_suite_provider(TEST_CIPHER_SUITE),
3145 )
3146 .with_identity_provider(FailureIdentityProvider::new())
3147 .receive([Proposal::GroupContextExtensions(
3148 make_external_senders_extension().await,
3149 )])
3150 .await;
3151
3152 assert_matches!(res, Err(MlsError::IdentityProviderError(_)));
3153 }
3154
3155 #[cfg(feature = "by_ref_proposal")]
3156 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_additional_invalid_external_senders_extension_fails()3157 async fn sending_additional_invalid_external_senders_extension_fails() {
3158 let (alice, tree) = new_tree("alice").await;
3159
3160 let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
3161 .with_identity_provider(FailureIdentityProvider::new())
3162 .with_additional([Proposal::GroupContextExtensions(
3163 make_external_senders_extension().await,
3164 )])
3165 .send()
3166 .await;
3167
3168 assert_matches!(res, Err(MlsError::IdentityProviderError(_)));
3169 }
3170
3171 #[cfg(feature = "by_ref_proposal")]
3172 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_invalid_external_senders_extension_filters_it_out()3173 async fn sending_invalid_external_senders_extension_filters_it_out() {
3174 let (alice, tree) = new_tree("alice").await;
3175
3176 let proposal = Proposal::GroupContextExtensions(make_external_senders_extension().await);
3177
3178 let proposal_info = make_proposal_info(&proposal, alice).await;
3179
3180 let processed_proposals =
3181 CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
3182 .with_identity_provider(FailureIdentityProvider::new())
3183 .cache(
3184 proposal_info.proposal_ref().unwrap().clone(),
3185 proposal.clone(),
3186 alice,
3187 )
3188 .send()
3189 .await
3190 .unwrap();
3191
3192 assert_eq!(processed_proposals.0, Vec::new());
3193
3194 #[cfg(feature = "state_update")]
3195 assert_eq!(processed_proposals.1.unused_proposals, vec![proposal_info]);
3196 }
3197
3198 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
receiving_reinit_with_other_proposals_fails()3199 async fn receiving_reinit_with_other_proposals_fails() {
3200 let (alice, tree) = new_tree("alice").await;
3201
3202 let res = CommitReceiver::new(
3203 &tree,
3204 alice,
3205 alice,
3206 test_cipher_suite_provider(TEST_CIPHER_SUITE),
3207 )
3208 .receive([
3209 Proposal::ReInit(make_reinit(TEST_PROTOCOL_VERSION)),
3210 Proposal::Add(make_add_proposal().await),
3211 ])
3212 .await;
3213
3214 assert_matches!(res, Err(MlsError::OtherProposalWithReInit));
3215 }
3216
3217 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_additional_reinit_with_other_proposals_fails()3218 async fn sending_additional_reinit_with_other_proposals_fails() {
3219 let (alice, tree) = new_tree("alice").await;
3220
3221 let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
3222 .with_additional([
3223 Proposal::ReInit(make_reinit(TEST_PROTOCOL_VERSION)),
3224 Proposal::Add(make_add_proposal().await),
3225 ])
3226 .send()
3227 .await;
3228
3229 assert_matches!(res, Err(MlsError::OtherProposalWithReInit));
3230 }
3231
3232 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_reinit_with_other_proposals_filters_it_out()3233 async fn sending_reinit_with_other_proposals_filters_it_out() {
3234 let (alice, tree) = new_tree("alice").await;
3235 let reinit = Proposal::ReInit(make_reinit(TEST_PROTOCOL_VERSION));
3236 let reinit_info = make_proposal_info(&reinit, alice).await;
3237 let add = Proposal::Add(make_add_proposal().await);
3238 let add_ref = make_proposal_ref(&add, alice).await;
3239
3240 let processed_proposals =
3241 CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
3242 .cache(
3243 reinit_info.proposal_ref().unwrap().clone(),
3244 reinit.clone(),
3245 alice,
3246 )
3247 .cache(add_ref.clone(), add, alice)
3248 .send()
3249 .await
3250 .unwrap();
3251
3252 assert_eq!(processed_proposals.0, vec![add_ref.into()]);
3253
3254 #[cfg(feature = "state_update")]
3255 assert_eq!(processed_proposals.1.unused_proposals, vec![reinit_info]);
3256 }
3257
3258 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
receiving_multiple_reinits_fails()3259 async fn receiving_multiple_reinits_fails() {
3260 let (alice, tree) = new_tree("alice").await;
3261
3262 let res = CommitReceiver::new(
3263 &tree,
3264 alice,
3265 alice,
3266 test_cipher_suite_provider(TEST_CIPHER_SUITE),
3267 )
3268 .receive([
3269 Proposal::ReInit(make_reinit(TEST_PROTOCOL_VERSION)),
3270 Proposal::ReInit(make_reinit(TEST_PROTOCOL_VERSION)),
3271 ])
3272 .await;
3273
3274 assert_matches!(res, Err(MlsError::OtherProposalWithReInit));
3275 }
3276
3277 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_additional_multiple_reinits_fails()3278 async fn sending_additional_multiple_reinits_fails() {
3279 let (alice, tree) = new_tree("alice").await;
3280
3281 let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
3282 .with_additional([
3283 Proposal::ReInit(make_reinit(TEST_PROTOCOL_VERSION)),
3284 Proposal::ReInit(make_reinit(TEST_PROTOCOL_VERSION)),
3285 ])
3286 .send()
3287 .await;
3288
3289 assert_matches!(res, Err(MlsError::OtherProposalWithReInit));
3290 }
3291
3292 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_multiple_reinits_keeps_only_one()3293 async fn sending_multiple_reinits_keeps_only_one() {
3294 let (alice, tree) = new_tree("alice").await;
3295 let reinit = Proposal::ReInit(make_reinit(TEST_PROTOCOL_VERSION));
3296 let reinit_ref = make_proposal_ref(&reinit, alice).await;
3297 let other_reinit = Proposal::ReInit(ReInitProposal {
3298 group_id: b"other_group".to_vec(),
3299 ..make_reinit(TEST_PROTOCOL_VERSION)
3300 });
3301 let other_reinit_ref = make_proposal_ref(&other_reinit, alice).await;
3302
3303 let processed_proposals =
3304 CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
3305 .cache(reinit_ref.clone(), reinit.clone(), alice)
3306 .cache(other_reinit_ref.clone(), other_reinit.clone(), alice)
3307 .send()
3308 .await
3309 .unwrap();
3310
3311 let processed_ref = match &*processed_proposals.0 {
3312 [ProposalOrRef::Reference(r)] => r,
3313 p => panic!("Expected single proposal reference but found {p:?}"),
3314 };
3315
3316 assert!(*processed_ref == reinit_ref || *processed_ref == other_reinit_ref);
3317
3318 #[cfg(feature = "state_update")]
3319 {
3320 let (rejected_ref, unused_proposal) = match &*processed_proposals.1.unused_proposals {
3321 [r] => (r.proposal_ref().unwrap().clone(), r.proposal.clone()),
3322 p => panic!("Expected single proposal but found {p:?}"),
3323 };
3324
3325 assert_ne!(rejected_ref, *processed_ref);
3326 assert!(rejected_ref == reinit_ref || rejected_ref == other_reinit_ref);
3327 assert!(unused_proposal == reinit || unused_proposal == other_reinit);
3328 }
3329 }
3330
make_external_init() -> ExternalInit3331 fn make_external_init() -> ExternalInit {
3332 ExternalInit {
3333 kem_output: vec![33; test_cipher_suite_provider(TEST_CIPHER_SUITE).kdf_extract_size()],
3334 }
3335 }
3336
3337 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
receiving_external_init_from_member_fails()3338 async fn receiving_external_init_from_member_fails() {
3339 let (alice, tree) = new_tree("alice").await;
3340
3341 let res = CommitReceiver::new(
3342 &tree,
3343 alice,
3344 alice,
3345 test_cipher_suite_provider(TEST_CIPHER_SUITE),
3346 )
3347 .receive([Proposal::ExternalInit(make_external_init())])
3348 .await;
3349
3350 assert_matches!(res, Err(MlsError::InvalidProposalTypeForSender));
3351 }
3352
3353 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_additional_external_init_from_member_fails()3354 async fn sending_additional_external_init_from_member_fails() {
3355 let (alice, tree) = new_tree("alice").await;
3356
3357 let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
3358 .with_additional([Proposal::ExternalInit(make_external_init())])
3359 .send()
3360 .await;
3361
3362 assert_matches!(res, Err(MlsError::InvalidProposalTypeForSender));
3363 }
3364
3365 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_external_init_from_member_filters_it_out()3366 async fn sending_external_init_from_member_filters_it_out() {
3367 let (alice, tree) = new_tree("alice").await;
3368 let external_init = Proposal::ExternalInit(make_external_init());
3369 let external_init_info = make_proposal_info(&external_init, alice).await;
3370
3371 let processed_proposals =
3372 CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
3373 .cache(
3374 external_init_info.proposal_ref().unwrap().clone(),
3375 external_init.clone(),
3376 alice,
3377 )
3378 .send()
3379 .await
3380 .unwrap();
3381
3382 assert_eq!(processed_proposals.0, Vec::new());
3383
3384 #[cfg(feature = "state_update")]
3385 assert_eq!(
3386 processed_proposals.1.unused_proposals,
3387 vec![external_init_info]
3388 );
3389 }
3390
required_capabilities_proposal(extension: u16) -> Proposal3391 fn required_capabilities_proposal(extension: u16) -> Proposal {
3392 let required_capabilities = RequiredCapabilitiesExt {
3393 extensions: vec![extension.into()],
3394 ..Default::default()
3395 };
3396
3397 let ext = vec![required_capabilities.into_extension().unwrap()];
3398
3399 Proposal::GroupContextExtensions(ext.into())
3400 }
3401
3402 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
receiving_required_capabilities_not_supported_by_member_fails()3403 async fn receiving_required_capabilities_not_supported_by_member_fails() {
3404 let (alice, tree) = new_tree("alice").await;
3405
3406 let res = CommitReceiver::new(
3407 &tree,
3408 alice,
3409 alice,
3410 test_cipher_suite_provider(TEST_CIPHER_SUITE),
3411 )
3412 .receive([required_capabilities_proposal(33)])
3413 .await;
3414
3415 assert_matches!(
3416 res,
3417 Err(MlsError::RequiredExtensionNotFound(v)) if v == 33.into()
3418 );
3419 }
3420
3421 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_required_capabilities_not_supported_by_member_fails()3422 async fn sending_required_capabilities_not_supported_by_member_fails() {
3423 let (alice, tree) = new_tree("alice").await;
3424
3425 let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
3426 .with_additional([required_capabilities_proposal(33)])
3427 .send()
3428 .await;
3429
3430 assert_matches!(
3431 res,
3432 Err(MlsError::RequiredExtensionNotFound(v)) if v == 33.into()
3433 );
3434 }
3435
3436 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_additional_required_capabilities_not_supported_by_member_filters_it_out()3437 async fn sending_additional_required_capabilities_not_supported_by_member_filters_it_out() {
3438 let (alice, tree) = new_tree("alice").await;
3439
3440 let proposal = required_capabilities_proposal(33);
3441 let proposal_info = make_proposal_info(&proposal, alice).await;
3442
3443 let processed_proposals =
3444 CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
3445 .cache(
3446 proposal_info.proposal_ref().unwrap().clone(),
3447 proposal.clone(),
3448 alice,
3449 )
3450 .send()
3451 .await
3452 .unwrap();
3453
3454 assert_eq!(processed_proposals.0, Vec::new());
3455
3456 #[cfg(feature = "state_update")]
3457 assert_eq!(processed_proposals.1.unused_proposals, vec![proposal_info]);
3458 }
3459
3460 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
committing_update_from_pk1_to_pk2_and_update_from_pk2_to_pk3_works()3461 async fn committing_update_from_pk1_to_pk2_and_update_from_pk2_to_pk3_works() {
3462 let (alice_leaf, alice_secret, alice_signer) =
3463 get_basic_test_node_sig_key(TEST_CIPHER_SUITE, "alice").await;
3464
3465 let (mut tree, priv_tree) = TreeKemPublic::derive(
3466 alice_leaf.clone(),
3467 alice_secret,
3468 &BasicIdentityProvider,
3469 &Default::default(),
3470 )
3471 .await
3472 .unwrap();
3473
3474 let alice = priv_tree.self_index;
3475
3476 let bob = add_member(&mut tree, "bob").await;
3477 let carol = add_member(&mut tree, "carol").await;
3478
3479 let bob_current_leaf = tree.get_leaf_node(bob).unwrap();
3480
3481 let mut alice_new_leaf = LeafNode {
3482 public_key: bob_current_leaf.public_key.clone(),
3483 leaf_node_source: LeafNodeSource::Update,
3484 ..alice_leaf
3485 };
3486
3487 alice_new_leaf
3488 .sign(
3489 &test_cipher_suite_provider(TEST_CIPHER_SUITE),
3490 &alice_signer,
3491 &(TEST_GROUP, 0).into(),
3492 )
3493 .await
3494 .unwrap();
3495
3496 let bob_new_leaf = update_leaf_node("bob", 1).await;
3497
3498 let pk1_to_pk2 = Proposal::Update(UpdateProposal {
3499 leaf_node: alice_new_leaf.clone(),
3500 });
3501
3502 let pk1_to_pk2_ref = make_proposal_ref(&pk1_to_pk2, alice).await;
3503
3504 let pk2_to_pk3 = Proposal::Update(UpdateProposal {
3505 leaf_node: bob_new_leaf.clone(),
3506 });
3507
3508 let pk2_to_pk3_ref = make_proposal_ref(&pk2_to_pk3, bob).await;
3509
3510 let effects = CommitReceiver::new(
3511 &tree,
3512 carol,
3513 carol,
3514 test_cipher_suite_provider(TEST_CIPHER_SUITE),
3515 )
3516 .cache(pk1_to_pk2_ref.clone(), pk1_to_pk2, alice)
3517 .cache(pk2_to_pk3_ref.clone(), pk2_to_pk3, bob)
3518 .receive([pk1_to_pk2_ref, pk2_to_pk3_ref])
3519 .await
3520 .unwrap();
3521
3522 assert_eq!(effects.applied_proposals.update_senders, vec![alice, bob]);
3523
3524 assert_eq!(
3525 effects
3526 .applied_proposals
3527 .updates
3528 .into_iter()
3529 .map(|p| p.proposal.leaf_node)
3530 .collect_vec(),
3531 vec![alice_new_leaf, bob_new_leaf]
3532 );
3533 }
3534
3535 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
committing_update_from_pk1_to_pk2_and_removal_of_pk2_works()3536 async fn committing_update_from_pk1_to_pk2_and_removal_of_pk2_works() {
3537 let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
3538
3539 let (alice_leaf, alice_secret, alice_signer) =
3540 get_basic_test_node_sig_key(TEST_CIPHER_SUITE, "alice").await;
3541
3542 let (mut tree, priv_tree) = TreeKemPublic::derive(
3543 alice_leaf.clone(),
3544 alice_secret,
3545 &BasicIdentityProvider,
3546 &Default::default(),
3547 )
3548 .await
3549 .unwrap();
3550
3551 let alice = priv_tree.self_index;
3552
3553 let bob = add_member(&mut tree, "bob").await;
3554 let carol = add_member(&mut tree, "carol").await;
3555
3556 let bob_current_leaf = tree.get_leaf_node(bob).unwrap();
3557
3558 let mut alice_new_leaf = LeafNode {
3559 public_key: bob_current_leaf.public_key.clone(),
3560 leaf_node_source: LeafNodeSource::Update,
3561 ..alice_leaf
3562 };
3563
3564 alice_new_leaf
3565 .sign(
3566 &cipher_suite_provider,
3567 &alice_signer,
3568 &(TEST_GROUP, 0).into(),
3569 )
3570 .await
3571 .unwrap();
3572
3573 let pk1_to_pk2 = Proposal::Update(UpdateProposal {
3574 leaf_node: alice_new_leaf.clone(),
3575 });
3576
3577 let pk1_to_pk2_ref = make_proposal_ref(&pk1_to_pk2, alice).await;
3578
3579 let remove_pk2 = Proposal::Remove(RemoveProposal { to_remove: bob });
3580
3581 let remove_pk2_ref = make_proposal_ref(&remove_pk2, bob).await;
3582
3583 let effects = CommitReceiver::new(
3584 &tree,
3585 carol,
3586 carol,
3587 test_cipher_suite_provider(TEST_CIPHER_SUITE),
3588 )
3589 .cache(pk1_to_pk2_ref.clone(), pk1_to_pk2, alice)
3590 .cache(remove_pk2_ref.clone(), remove_pk2, bob)
3591 .receive([pk1_to_pk2_ref, remove_pk2_ref])
3592 .await
3593 .unwrap();
3594
3595 assert_eq!(effects.applied_proposals.update_senders, vec![alice]);
3596
3597 assert_eq!(
3598 effects
3599 .applied_proposals
3600 .updates
3601 .into_iter()
3602 .map(|p| p.proposal.leaf_node)
3603 .collect_vec(),
3604 vec![alice_new_leaf]
3605 );
3606
3607 assert_eq!(
3608 effects
3609 .applied_proposals
3610 .removals
3611 .into_iter()
3612 .map(|p| p.proposal.to_remove)
3613 .collect_vec(),
3614 vec![bob]
3615 );
3616 }
3617
3618 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
unsupported_credential_key_package(name: &str) -> KeyPackage3619 async fn unsupported_credential_key_package(name: &str) -> KeyPackage {
3620 let (mut signing_identity, secret_key) =
3621 get_test_signing_identity(TEST_CIPHER_SUITE, name.as_bytes()).await;
3622
3623 signing_identity.credential = Credential::Custom(CustomCredential::new(
3624 CredentialType::new(BasicWithCustomProvider::CUSTOM_CREDENTIAL_TYPE),
3625 random_bytes(32),
3626 ));
3627
3628 let generator = KeyPackageGenerator {
3629 protocol_version: TEST_PROTOCOL_VERSION,
3630 cipher_suite_provider: &test_cipher_suite_provider(TEST_CIPHER_SUITE),
3631 signing_identity: &signing_identity,
3632 signing_key: &secret_key,
3633 };
3634
3635 generator
3636 .generate(
3637 Lifetime::years(1).unwrap(),
3638 Capabilities {
3639 credentials: vec![42.into()],
3640 ..Default::default()
3641 },
3642 Default::default(),
3643 Default::default(),
3644 )
3645 .await
3646 .unwrap()
3647 .key_package
3648 }
3649
3650 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
receiving_add_with_leaf_not_supporting_credential_type_of_other_leaf_fails()3651 async fn receiving_add_with_leaf_not_supporting_credential_type_of_other_leaf_fails() {
3652 let (alice, tree) = new_tree("alice").await;
3653
3654 let res = CommitReceiver::new(
3655 &tree,
3656 alice,
3657 alice,
3658 test_cipher_suite_provider(TEST_CIPHER_SUITE),
3659 )
3660 .receive([Proposal::Add(Box::new(AddProposal {
3661 key_package: unsupported_credential_key_package("bob").await,
3662 }))])
3663 .await;
3664
3665 assert_matches!(res, Err(MlsError::InUseCredentialTypeUnsupportedByNewLeaf));
3666 }
3667
3668 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_additional_add_with_leaf_not_supporting_credential_type_of_other_leaf_fails()3669 async fn sending_additional_add_with_leaf_not_supporting_credential_type_of_other_leaf_fails() {
3670 let (alice, tree) = new_tree("alice").await;
3671
3672 let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
3673 .with_additional([Proposal::Add(Box::new(AddProposal {
3674 key_package: unsupported_credential_key_package("bob").await,
3675 }))])
3676 .send()
3677 .await;
3678
3679 assert_matches!(res, Err(MlsError::InUseCredentialTypeUnsupportedByNewLeaf));
3680 }
3681
3682 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_add_with_leaf_not_supporting_credential_type_of_other_leaf_filters_it_out()3683 async fn sending_add_with_leaf_not_supporting_credential_type_of_other_leaf_filters_it_out() {
3684 let (alice, tree) = new_tree("alice").await;
3685
3686 let add = Proposal::Add(Box::new(AddProposal {
3687 key_package: unsupported_credential_key_package("bob").await,
3688 }));
3689
3690 let add_info = make_proposal_info(&add, alice).await;
3691
3692 let processed_proposals =
3693 CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
3694 .cache(add_info.proposal_ref().unwrap().clone(), add.clone(), alice)
3695 .send()
3696 .await
3697 .unwrap();
3698
3699 assert_eq!(processed_proposals.0, Vec::new());
3700
3701 #[cfg(feature = "state_update")]
3702 assert_eq!(processed_proposals.1.unused_proposals, vec![add_info]);
3703 }
3704
3705 #[cfg(feature = "custom_proposal")]
3706 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_custom_proposal_with_member_not_supporting_proposal_type_fails()3707 async fn sending_custom_proposal_with_member_not_supporting_proposal_type_fails() {
3708 let (alice, tree) = new_tree("alice").await;
3709
3710 let custom_proposal = Proposal::Custom(CustomProposal::new(ProposalType::new(42), vec![]));
3711
3712 let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
3713 .with_additional([custom_proposal.clone()])
3714 .send()
3715 .await;
3716
3717 assert_matches!(
3718 res,
3719 Err(
3720 MlsError::UnsupportedCustomProposal(c)
3721 ) if c == custom_proposal.proposal_type()
3722 );
3723 }
3724
3725 #[cfg(feature = "custom_proposal")]
3726 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_custom_proposal_with_member_not_supporting_filters_it_out()3727 async fn sending_custom_proposal_with_member_not_supporting_filters_it_out() {
3728 let (alice, tree) = new_tree("alice").await;
3729
3730 let custom_proposal = Proposal::Custom(CustomProposal::new(ProposalType::new(42), vec![]));
3731
3732 let custom_info = make_proposal_info(&custom_proposal, alice).await;
3733
3734 let processed_proposals =
3735 CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
3736 .cache(
3737 custom_info.proposal_ref().unwrap().clone(),
3738 custom_proposal.clone(),
3739 alice,
3740 )
3741 .send()
3742 .await
3743 .unwrap();
3744
3745 assert_eq!(processed_proposals.0, Vec::new());
3746
3747 #[cfg(feature = "state_update")]
3748 assert_eq!(processed_proposals.1.unused_proposals, vec![custom_info]);
3749 }
3750
3751 #[cfg(feature = "custom_proposal")]
3752 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
receiving_custom_proposal_with_member_not_supporting_fails()3753 async fn receiving_custom_proposal_with_member_not_supporting_fails() {
3754 let (alice, tree) = new_tree("alice").await;
3755
3756 let custom_proposal = Proposal::Custom(CustomProposal::new(ProposalType::new(42), vec![]));
3757
3758 let res = CommitReceiver::new(
3759 &tree,
3760 alice,
3761 alice,
3762 test_cipher_suite_provider(TEST_CIPHER_SUITE),
3763 )
3764 .receive([custom_proposal.clone()])
3765 .await;
3766
3767 assert_matches!(
3768 res,
3769 Err(MlsError::UnsupportedCustomProposal(c)) if c == custom_proposal.proposal_type()
3770 );
3771 }
3772
3773 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
receiving_group_extension_unsupported_by_leaf_fails()3774 async fn receiving_group_extension_unsupported_by_leaf_fails() {
3775 let (alice, tree) = new_tree("alice").await;
3776
3777 let res = CommitReceiver::new(
3778 &tree,
3779 alice,
3780 alice,
3781 test_cipher_suite_provider(TEST_CIPHER_SUITE),
3782 )
3783 .receive([Proposal::GroupContextExtensions(make_extension_list(0))])
3784 .await;
3785
3786 assert_matches!(
3787 res,
3788 Err(
3789 MlsError::UnsupportedGroupExtension(v)
3790 ) if v == 42.into()
3791 );
3792 }
3793
3794 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_additional_group_extension_unsupported_by_leaf_fails()3795 async fn sending_additional_group_extension_unsupported_by_leaf_fails() {
3796 let (alice, tree) = new_tree("alice").await;
3797
3798 let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
3799 .with_additional([Proposal::GroupContextExtensions(make_extension_list(0))])
3800 .send()
3801 .await;
3802
3803 assert_matches!(
3804 res,
3805 Err(
3806 MlsError::UnsupportedGroupExtension(v)
3807 ) if v == 42.into()
3808 );
3809 }
3810
3811 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_group_extension_unsupported_by_leaf_filters_it_out()3812 async fn sending_group_extension_unsupported_by_leaf_filters_it_out() {
3813 let (alice, tree) = new_tree("alice").await;
3814
3815 let proposal = Proposal::GroupContextExtensions(make_extension_list(0));
3816 let proposal_info = make_proposal_info(&proposal, alice).await;
3817
3818 let processed_proposals =
3819 CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
3820 .cache(
3821 proposal_info.proposal_ref().unwrap().clone(),
3822 proposal.clone(),
3823 alice,
3824 )
3825 .send()
3826 .await
3827 .unwrap();
3828
3829 assert_eq!(processed_proposals.0, Vec::new());
3830
3831 #[cfg(feature = "state_update")]
3832 assert_eq!(processed_proposals.1.unused_proposals, vec![proposal_info]);
3833 }
3834
3835 #[cfg(feature = "psk")]
3836 #[derive(Debug)]
3837 struct AlwaysNotFoundPskStorage;
3838
3839 #[cfg(feature = "psk")]
3840 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
3841 #[cfg_attr(mls_build_async, maybe_async::must_be_async)]
3842 impl PreSharedKeyStorage for AlwaysNotFoundPskStorage {
3843 type Error = Infallible;
3844
get(&self, _: &ExternalPskId) -> Result<Option<PreSharedKey>, Self::Error>3845 async fn get(&self, _: &ExternalPskId) -> Result<Option<PreSharedKey>, Self::Error> {
3846 Ok(None)
3847 }
3848 }
3849
3850 #[cfg(feature = "psk")]
3851 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
receiving_external_psk_with_unknown_id_fails()3852 async fn receiving_external_psk_with_unknown_id_fails() {
3853 let (alice, tree) = new_tree("alice").await;
3854
3855 let res = CommitReceiver::new(
3856 &tree,
3857 alice,
3858 alice,
3859 test_cipher_suite_provider(TEST_CIPHER_SUITE),
3860 )
3861 .with_psk_storage(AlwaysNotFoundPskStorage)
3862 .receive([Proposal::Psk(new_external_psk(b"abc"))])
3863 .await;
3864
3865 assert_matches!(res, Err(MlsError::MissingRequiredPsk));
3866 }
3867
3868 #[cfg(feature = "psk")]
3869 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_additional_external_psk_with_unknown_id_fails()3870 async fn sending_additional_external_psk_with_unknown_id_fails() {
3871 let (alice, tree) = new_tree("alice").await;
3872
3873 let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
3874 .with_psk_storage(AlwaysNotFoundPskStorage)
3875 .with_additional([Proposal::Psk(new_external_psk(b"abc"))])
3876 .send()
3877 .await;
3878
3879 assert_matches!(res, Err(MlsError::MissingRequiredPsk));
3880 }
3881
3882 #[cfg(feature = "psk")]
3883 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
sending_external_psk_with_unknown_id_filters_it_out()3884 async fn sending_external_psk_with_unknown_id_filters_it_out() {
3885 let (alice, tree) = new_tree("alice").await;
3886 let proposal = Proposal::Psk(new_external_psk(b"abc"));
3887 let proposal_info = make_proposal_info(&proposal, alice).await;
3888
3889 let processed_proposals =
3890 CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
3891 .with_psk_storage(AlwaysNotFoundPskStorage)
3892 .cache(
3893 proposal_info.proposal_ref().unwrap().clone(),
3894 proposal.clone(),
3895 alice,
3896 )
3897 .send()
3898 .await
3899 .unwrap();
3900
3901 assert_eq!(processed_proposals.0, Vec::new());
3902
3903 #[cfg(feature = "state_update")]
3904 assert_eq!(processed_proposals.1.unused_proposals, vec![proposal_info]);
3905 }
3906
3907 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
user_defined_filter_can_remove_proposals()3908 async fn user_defined_filter_can_remove_proposals() {
3909 struct RemoveGroupContextExtensions;
3910
3911 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
3912 #[cfg_attr(mls_build_async, maybe_async::must_be_async)]
3913 impl MlsRules for RemoveGroupContextExtensions {
3914 type Error = Infallible;
3915
3916 async fn filter_proposals(
3917 &self,
3918 _: CommitDirection,
3919 _: CommitSource,
3920 _: &Roster,
3921 _: &ExtensionList,
3922 mut proposals: ProposalBundle,
3923 ) -> Result<ProposalBundle, Self::Error> {
3924 proposals.group_context_extensions.clear();
3925 Ok(proposals)
3926 }
3927
3928 #[cfg_attr(coverage_nightly, coverage(off))]
3929 fn commit_options(
3930 &self,
3931 _: &Roster,
3932 _: &ExtensionList,
3933 _: &ProposalBundle,
3934 ) -> Result<CommitOptions, Self::Error> {
3935 Ok(Default::default())
3936 }
3937
3938 #[cfg_attr(coverage_nightly, coverage(off))]
3939 fn encryption_options(
3940 &self,
3941 _: &Roster,
3942 _: &ExtensionList,
3943 ) -> Result<EncryptionOptions, Self::Error> {
3944 Ok(Default::default())
3945 }
3946 }
3947
3948 let (alice, tree) = new_tree("alice").await;
3949
3950 let (committed, _) =
3951 CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
3952 .with_additional([Proposal::GroupContextExtensions(Default::default())])
3953 .with_user_rules(RemoveGroupContextExtensions)
3954 .send()
3955 .await
3956 .unwrap();
3957
3958 assert_eq!(committed, Vec::new());
3959 }
3960
3961 struct FailureMlsRules;
3962
3963 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
3964 #[cfg_attr(mls_build_async, maybe_async::must_be_async)]
3965 impl MlsRules for FailureMlsRules {
3966 type Error = MlsError;
3967
filter_proposals( &self, _: CommitDirection, _: CommitSource, _: &Roster, _: &ExtensionList, _: ProposalBundle, ) -> Result<ProposalBundle, Self::Error>3968 async fn filter_proposals(
3969 &self,
3970 _: CommitDirection,
3971 _: CommitSource,
3972 _: &Roster,
3973 _: &ExtensionList,
3974 _: ProposalBundle,
3975 ) -> Result<ProposalBundle, Self::Error> {
3976 Err(MlsError::InvalidSignature)
3977 }
3978
3979 #[cfg_attr(coverage_nightly, coverage(off))]
commit_options( &self, _: &Roster, _: &ExtensionList, _: &ProposalBundle, ) -> Result<CommitOptions, Self::Error>3980 fn commit_options(
3981 &self,
3982 _: &Roster,
3983 _: &ExtensionList,
3984 _: &ProposalBundle,
3985 ) -> Result<CommitOptions, Self::Error> {
3986 Ok(Default::default())
3987 }
3988
3989 #[cfg_attr(coverage_nightly, coverage(off))]
encryption_options( &self, _: &Roster, _: &ExtensionList, ) -> Result<EncryptionOptions, Self::Error>3990 fn encryption_options(
3991 &self,
3992 _: &Roster,
3993 _: &ExtensionList,
3994 ) -> Result<EncryptionOptions, Self::Error> {
3995 Ok(Default::default())
3996 }
3997 }
3998
3999 struct InjectMlsRules {
4000 to_inject: Proposal,
4001 source: ProposalSource,
4002 }
4003
4004 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
4005 #[cfg_attr(mls_build_async, maybe_async::must_be_async)]
4006 impl MlsRules for InjectMlsRules {
4007 type Error = MlsError;
4008
filter_proposals( &self, _: CommitDirection, _: CommitSource, _: &Roster, _: &ExtensionList, mut proposals: ProposalBundle, ) -> Result<ProposalBundle, Self::Error>4009 async fn filter_proposals(
4010 &self,
4011 _: CommitDirection,
4012 _: CommitSource,
4013 _: &Roster,
4014 _: &ExtensionList,
4015 mut proposals: ProposalBundle,
4016 ) -> Result<ProposalBundle, Self::Error> {
4017 proposals.add(
4018 self.to_inject.clone(),
4019 Sender::Member(0),
4020 self.source.clone(),
4021 );
4022 Ok(proposals)
4023 }
4024
4025 #[cfg_attr(coverage_nightly, coverage(off))]
commit_options( &self, _: &Roster, _: &ExtensionList, _: &ProposalBundle, ) -> Result<CommitOptions, Self::Error>4026 fn commit_options(
4027 &self,
4028 _: &Roster,
4029 _: &ExtensionList,
4030 _: &ProposalBundle,
4031 ) -> Result<CommitOptions, Self::Error> {
4032 Ok(Default::default())
4033 }
4034
4035 #[cfg_attr(coverage_nightly, coverage(off))]
encryption_options( &self, _: &Roster, _: &ExtensionList, ) -> Result<EncryptionOptions, Self::Error>4036 fn encryption_options(
4037 &self,
4038 _: &Roster,
4039 _: &ExtensionList,
4040 ) -> Result<EncryptionOptions, Self::Error> {
4041 Ok(Default::default())
4042 }
4043 }
4044
4045 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
user_defined_filter_can_inject_proposals()4046 async fn user_defined_filter_can_inject_proposals() {
4047 let (alice, tree) = new_tree("alice").await;
4048
4049 let test_proposal = Proposal::GroupContextExtensions(Default::default());
4050
4051 let (committed, _) =
4052 CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
4053 .with_user_rules(InjectMlsRules {
4054 to_inject: test_proposal.clone(),
4055 source: ProposalSource::ByValue,
4056 })
4057 .send()
4058 .await
4059 .unwrap();
4060
4061 assert_eq!(
4062 committed,
4063 vec![ProposalOrRef::Proposal(test_proposal.into())]
4064 );
4065 }
4066
4067 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
user_defined_filter_can_inject_local_only_proposals()4068 async fn user_defined_filter_can_inject_local_only_proposals() {
4069 let (alice, tree) = new_tree("alice").await;
4070
4071 let test_proposal = Proposal::GroupContextExtensions(Default::default());
4072
4073 let (committed, _) =
4074 CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
4075 .with_user_rules(InjectMlsRules {
4076 to_inject: test_proposal.clone(),
4077 source: ProposalSource::Local,
4078 })
4079 .send()
4080 .await
4081 .unwrap();
4082
4083 assert_eq!(committed, vec![]);
4084 }
4085
4086 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
user_defined_filter_cant_break_base_rules()4087 async fn user_defined_filter_cant_break_base_rules() {
4088 let (alice, tree) = new_tree("alice").await;
4089
4090 let test_proposal = Proposal::Update(UpdateProposal {
4091 leaf_node: get_basic_test_node(TEST_CIPHER_SUITE, "leaf").await,
4092 });
4093
4094 let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
4095 .with_user_rules(InjectMlsRules {
4096 to_inject: test_proposal.clone(),
4097 source: ProposalSource::ByValue,
4098 })
4099 .send()
4100 .await;
4101
4102 assert_matches!(res, Err(MlsError::InvalidProposalTypeForSender { .. }))
4103 }
4104
4105 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
user_defined_filter_can_refuse_to_send_commit()4106 async fn user_defined_filter_can_refuse_to_send_commit() {
4107 let (alice, tree) = new_tree("alice").await;
4108
4109 let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
4110 .with_additional([Proposal::GroupContextExtensions(Default::default())])
4111 .with_user_rules(FailureMlsRules)
4112 .send()
4113 .await;
4114
4115 assert_matches!(res, Err(MlsError::MlsRulesError(_)));
4116 }
4117
4118 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
user_defined_filter_can_reject_incoming_commit()4119 async fn user_defined_filter_can_reject_incoming_commit() {
4120 let (alice, tree) = new_tree("alice").await;
4121
4122 let res = CommitReceiver::new(
4123 &tree,
4124 alice,
4125 alice,
4126 test_cipher_suite_provider(TEST_CIPHER_SUITE),
4127 )
4128 .with_user_rules(FailureMlsRules)
4129 .receive([Proposal::GroupContextExtensions(Default::default())])
4130 .await;
4131
4132 assert_matches!(res, Err(MlsError::MlsRulesError(_)));
4133 }
4134
4135 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
proposers_are_verified()4136 async fn proposers_are_verified() {
4137 let (alice, mut tree) = new_tree("alice").await;
4138 let bob = add_member(&mut tree, "bob").await;
4139
4140 #[cfg(feature = "by_ref_proposal")]
4141 let identity = get_test_signing_identity(TEST_CIPHER_SUITE, b"carol")
4142 .await
4143 .0;
4144
4145 #[cfg(feature = "by_ref_proposal")]
4146 let external_senders = ExternalSendersExt::new(vec![identity]);
4147
4148 let proposals: &[Proposal] = &[
4149 Proposal::Add(make_add_proposal().await),
4150 Proposal::Update(make_update_proposal("alice").await),
4151 Proposal::Remove(RemoveProposal { to_remove: bob }),
4152 #[cfg(feature = "psk")]
4153 Proposal::Psk(make_external_psk(
4154 b"ted",
4155 PskNonce::random(&test_cipher_suite_provider(TEST_CIPHER_SUITE)).unwrap(),
4156 )),
4157 Proposal::ReInit(make_reinit(TEST_PROTOCOL_VERSION)),
4158 Proposal::ExternalInit(make_external_init()),
4159 Proposal::GroupContextExtensions(Default::default()),
4160 ];
4161
4162 let proposers = [
4163 Sender::Member(*alice),
4164 #[cfg(feature = "by_ref_proposal")]
4165 Sender::External(0),
4166 Sender::NewMemberCommit,
4167 Sender::NewMemberProposal,
4168 ];
4169
4170 for ((proposer, proposal), by_ref) in proposers
4171 .into_iter()
4172 .cartesian_product(proposals)
4173 .cartesian_product([true])
4174 {
4175 let committer = Sender::Member(*alice);
4176
4177 let receiver = CommitReceiver::new(
4178 &tree,
4179 committer,
4180 alice,
4181 test_cipher_suite_provider(TEST_CIPHER_SUITE),
4182 );
4183
4184 #[cfg(feature = "by_ref_proposal")]
4185 let extensions: ExtensionList =
4186 vec![external_senders.clone().into_extension().unwrap()].into();
4187
4188 #[cfg(feature = "by_ref_proposal")]
4189 let receiver = receiver.with_extensions(extensions);
4190
4191 let (receiver, proposals, proposer) = if by_ref {
4192 let proposal_ref = make_proposal_ref(proposal, proposer).await;
4193 let receiver = receiver.cache(proposal_ref.clone(), proposal.clone(), proposer);
4194 (receiver, vec![ProposalOrRef::from(proposal_ref)], proposer)
4195 } else {
4196 (receiver, vec![proposal.clone().into()], committer)
4197 };
4198
4199 let res = receiver.receive(proposals).await;
4200
4201 if proposer_can_propose(proposer, proposal.proposal_type(), by_ref).is_err() {
4202 assert_matches!(res, Err(MlsError::InvalidProposalTypeForSender));
4203 } else {
4204 let is_self_update = proposal.proposal_type() == ProposalType::UPDATE
4205 && by_ref
4206 && matches!(proposer, Sender::Member(_));
4207
4208 if !is_self_update {
4209 res.unwrap();
4210 }
4211 }
4212 }
4213 }
4214
4215 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
make_update_proposal(name: &str) -> UpdateProposal4216 async fn make_update_proposal(name: &str) -> UpdateProposal {
4217 UpdateProposal {
4218 leaf_node: update_leaf_node(name, 1).await,
4219 }
4220 }
4221
4222 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
make_update_proposal_custom(name: &str, leaf_index: u32) -> UpdateProposal4223 async fn make_update_proposal_custom(name: &str, leaf_index: u32) -> UpdateProposal {
4224 UpdateProposal {
4225 leaf_node: update_leaf_node(name, leaf_index).await,
4226 }
4227 }
4228
4229 #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
when_receiving_commit_unused_proposals_are_proposals_in_cache_but_not_in_commit()4230 async fn when_receiving_commit_unused_proposals_are_proposals_in_cache_but_not_in_commit() {
4231 let (alice, tree) = new_tree("alice").await;
4232
4233 let proposal = Proposal::GroupContextExtensions(Default::default());
4234 let proposal_ref = make_proposal_ref(&proposal, alice).await;
4235
4236 let state = CommitReceiver::new(
4237 &tree,
4238 alice,
4239 alice,
4240 test_cipher_suite_provider(TEST_CIPHER_SUITE),
4241 )
4242 .cache(proposal_ref.clone(), proposal, alice)
4243 .receive([Proposal::Add(Box::new(AddProposal {
4244 key_package: test_key_package(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "bob").await,
4245 }))])
4246 .await
4247 .unwrap();
4248
4249 let [p] = &state.unused_proposals[..] else {
4250 panic!(
4251 "Expected single unused proposal but got {:?}",
4252 state.unused_proposals
4253 );
4254 };
4255
4256 assert_eq!(p.proposal_ref(), Some(&proposal_ref));
4257 }
4258 }
4259