Revision control
Copy as Markdown
Other Tools
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// Copyright by contributors to this project.
// SPDX-License-Identifier: (Apache-2.0 OR MIT)
use alloc::vec::Vec;
use super::{
message_processor::ProvisionalState,
mls_rules::{CommitDirection, CommitSource, MlsRules},
GroupState, ProposalOrRef,
};
use crate::{
client::MlsError,
group::{
proposal_filter::{ProposalApplier, ProposalBundle, ProposalSource},
Proposal, Sender,
},
time::MlsTime,
};
#[cfg(feature = "by_ref_proposal")]
use crate::group::{proposal_filter::FilterStrategy, ProposalRef, ProtocolVersion};
use crate::tree_kem::leaf_node::LeafNode;
#[cfg(all(feature = "std", feature = "by_ref_proposal"))]
use std::collections::HashMap;
#[cfg(feature = "by_ref_proposal")]
use mls_rs_codec::{MlsDecode, MlsEncode, MlsSize};
use mls_rs_core::{
crypto::CipherSuiteProvider, error::IntoAnyError, identity::IdentityProvider,
psk::PreSharedKeyStorage,
};
#[cfg(feature = "by_ref_proposal")]
use core::fmt::{self, Debug};
#[cfg(feature = "by_ref_proposal")]
#[derive(Debug, Clone, MlsSize, MlsEncode, MlsDecode, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CachedProposal {
pub(crate) proposal: Proposal,
pub(crate) sender: Sender,
}
#[cfg(feature = "by_ref_proposal")]
#[derive(Clone, PartialEq)]
pub(crate) struct ProposalCache {
protocol_version: ProtocolVersion,
group_id: Vec<u8>,
#[cfg(feature = "std")]
pub(crate) proposals: HashMap<ProposalRef, CachedProposal>,
#[cfg(not(feature = "std"))]
pub(crate) proposals: Vec<(ProposalRef, CachedProposal)>,
}
#[cfg(feature = "by_ref_proposal")]
impl Debug for ProposalCache {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ProposalCache")
.field("protocol_version", &self.protocol_version)
.field(
"group_id",
&mls_rs_core::debug::pretty_group_id(&self.group_id),
)
.field("proposals", &self.proposals)
.finish()
}
}
#[cfg(feature = "by_ref_proposal")]
impl ProposalCache {
pub fn new(protocol_version: ProtocolVersion, group_id: Vec<u8>) -> Self {
Self {
protocol_version,
group_id,
proposals: Default::default(),
}
}
pub fn import(
protocol_version: ProtocolVersion,
group_id: Vec<u8>,
#[cfg(feature = "std")] proposals: HashMap<ProposalRef, CachedProposal>,
#[cfg(not(feature = "std"))] proposals: Vec<(ProposalRef, CachedProposal)>,
) -> Self {
Self {
protocol_version,
group_id,
proposals,
}
}
#[inline]
pub fn clear(&mut self) {
self.proposals.clear();
}
#[cfg(feature = "private_message")]
#[inline]
pub fn is_empty(&self) -> bool {
self.proposals.is_empty()
}
pub fn insert(&mut self, proposal_ref: ProposalRef, proposal: Proposal, sender: Sender) {
let cached_proposal = CachedProposal { proposal, sender };
#[cfg(feature = "std")]
self.proposals.insert(proposal_ref, cached_proposal);
#[cfg(not(feature = "std"))]
// This may result in dups but it does not matter
self.proposals.push((proposal_ref, cached_proposal));
}
pub fn prepare_commit(
&self,
sender: Sender,
additional_proposals: Vec<Proposal>,
) -> ProposalBundle {
self.proposals
.iter()
.map(|(r, p)| {
(
p.proposal.clone(),
p.sender,
ProposalSource::ByReference(r.clone()),
)
})
.chain(
additional_proposals
.into_iter()
.map(|p| (p, sender, ProposalSource::ByValue)),
)
.collect()
}
pub fn resolve_for_commit(
&self,
sender: Sender,
proposal_list: Vec<ProposalOrRef>,
) -> Result<ProposalBundle, MlsError> {
let mut proposals = ProposalBundle::default();
for p in proposal_list {
match p {
ProposalOrRef::Proposal(p) => proposals.add(*p, sender, ProposalSource::ByValue),
ProposalOrRef::Reference(r) => {
#[cfg(feature = "std")]
let p = self
.proposals
.get(&r)
.ok_or(MlsError::ProposalNotFound)?
.clone();
#[cfg(not(feature = "std"))]
let p = self
.proposals
.iter()
.find_map(|(rr, p)| (rr == &r).then_some(p))
.ok_or(MlsError::ProposalNotFound)?
.clone();
proposals.add(p.proposal, p.sender, ProposalSource::ByReference(r));
}
};
}
Ok(proposals)
}
}
#[cfg(not(feature = "by_ref_proposal"))]
pub(crate) fn prepare_commit(
sender: Sender,
additional_proposals: Vec<Proposal>,
) -> ProposalBundle {
let mut proposals = ProposalBundle::default();
for p in additional_proposals.into_iter() {
proposals.add(p, sender, ProposalSource::ByValue);
}
proposals
}
#[cfg(not(feature = "by_ref_proposal"))]
pub(crate) fn resolve_for_commit(
sender: Sender,
proposal_list: Vec<ProposalOrRef>,
) -> Result<ProposalBundle, MlsError> {
let mut proposals = ProposalBundle::default();
for p in proposal_list {
let ProposalOrRef::Proposal(p) = p;
proposals.add(*p, sender, ProposalSource::ByValue);
}
Ok(proposals)
}
impl GroupState {
#[inline(never)]
#[allow(clippy::too_many_arguments)]
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
pub(crate) async fn 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,
{
let roster = self.public_tree.roster();
let group_extensions = &self.context.extensions;
#[cfg(feature = "by_ref_proposal")]
let all_proposals = proposals.clone();
let origin = match sender {
Sender::Member(index) => Ok::<_, MlsError>(CommitSource::ExistingMember(
roster.member_with_index(index)?,
)),
#[cfg(feature = "by_ref_proposal")]
Sender::NewMemberProposal => Err(MlsError::InvalidSender),
#[cfg(feature = "by_ref_proposal")]
Sender::External(_) => Err(MlsError::InvalidSender),
Sender::NewMemberCommit => Ok(CommitSource::NewMember(
external_leaf
.map(|l| l.signing_identity.clone())
.ok_or(MlsError::ExternalCommitMustHaveNewLeaf)?,
)),
}?;
proposals = user_rules
.filter_proposals(direction, origin, &roster, group_extensions, proposals)
.await
.map_err(|e| MlsError::MlsRulesError(e.into_any_error()))?;
let applier = ProposalApplier::new(
&self.public_tree,
self.context.protocol_version,
cipher_suite_provider,
group_extensions,
external_leaf,
identity_provider,
psk_storage,
#[cfg(feature = "by_ref_proposal")]
&self.context.group_id,
);
#[cfg(feature = "by_ref_proposal")]
let applier_output = match direction {
CommitDirection::Send => {
applier
.apply_proposals(FilterStrategy::IgnoreByRef, &sender, proposals, commit_time)
.await?
}
CommitDirection::Receive => {
applier
.apply_proposals(FilterStrategy::IgnoreNone, &sender, proposals, commit_time)
.await?
}
};
#[cfg(not(feature = "by_ref_proposal"))]
let applier_output = applier
.apply_proposals(&sender, &proposals, commit_time)
.await?;
#[cfg(feature = "by_ref_proposal")]
let unused_proposals = unused_proposals(
match direction {
CommitDirection::Send => all_proposals,
CommitDirection::Receive => self.proposals.proposals.iter().collect(),
},
&applier_output.applied_proposals,
);
let mut group_context = self.context.clone();
group_context.epoch += 1;
if let Some(ext) = applier_output.new_context_extensions {
group_context.extensions = ext;
}
#[cfg(feature = "by_ref_proposal")]
let proposals = applier_output.applied_proposals;
Ok(ProvisionalState {
public_tree: applier_output.new_tree,
group_context,
applied_proposals: proposals,
external_init_index: applier_output.external_init_index,
indexes_of_added_kpkgs: applier_output.indexes_of_added_kpkgs,
#[cfg(feature = "by_ref_proposal")]
unused_proposals,
})
}
}
#[cfg(feature = "by_ref_proposal")]
impl Extend<(ProposalRef, CachedProposal)> for ProposalCache {
fn extend<T>(&mut self, iter: T)
where
T: IntoIterator<Item = (ProposalRef, CachedProposal)>,
{
self.proposals.extend(iter);
}
}
#[cfg(feature = "by_ref_proposal")]
fn has_ref(proposals: &ProposalBundle, reference: &ProposalRef) -> bool {
proposals
.iter_proposals()
.any(|p| matches!(&p.source, ProposalSource::ByReference(r) if r == reference))
}
#[cfg(feature = "by_ref_proposal")]
fn unused_proposals(
all_proposals: ProposalBundle,
accepted_proposals: &ProposalBundle,
) -> Vec<crate::mls_rules::ProposalInfo<Proposal>> {
all_proposals
.into_proposals()
.filter(|p| {
matches!(p.source, ProposalSource::ByReference(ref r) if !has_ref(accepted_proposals, r)
)
})
.collect()
}
// TODO add tests for lite version of filtering
#[cfg(all(feature = "by_ref_proposal", test))]
pub(crate) mod test_utils {
use mls_rs_core::{
crypto::CipherSuiteProvider, extension::ExtensionList, identity::IdentityProvider,
psk::PreSharedKeyStorage,
};
use crate::{
client::test_utils::TEST_PROTOCOL_VERSION,
group::{
confirmation_tag::ConfirmationTag,
mls_rules::{CommitDirection, DefaultMlsRules, MlsRules},
proposal::{Proposal, ProposalOrRef},
proposal_ref::ProposalRef,
state::GroupState,
test_utils::{get_test_group_context, TEST_GROUP},
GroupContext, LeafIndex, LeafNode, ProvisionalState, Sender, TreeKemPublic,
},
identity::{basic::BasicIdentityProvider, test_utils::BasicWithCustomProvider},
psk::AlwaysFoundPskStorage,
};
use super::{CachedProposal, MlsError, ProposalCache};
use alloc::vec::Vec;
impl CachedProposal {
pub fn new(proposal: Proposal, sender: Sender) -> Self {
Self { proposal, sender }
}
}
#[derive(Debug)]
pub(crate) struct CommitReceiver<'a, C, F, P, CSP> {
tree: &'a TreeKemPublic,
sender: Sender,
receiver: LeafIndex,
cache: ProposalCache,
identity_provider: C,
cipher_suite_provider: CSP,
group_context_extensions: ExtensionList,
user_rules: F,
with_psk_storage: P,
}
impl<'a, CSP>
CommitReceiver<'a, BasicWithCustomProvider, DefaultMlsRules, AlwaysFoundPskStorage, CSP>
{
pub fn new<S>(
tree: &'a TreeKemPublic,
sender: S,
receiver: LeafIndex,
cipher_suite_provider: CSP,
) -> Self
where
S: Into<Sender>,
{
Self {
tree,
sender: sender.into(),
receiver,
cache: make_proposal_cache(),
identity_provider: BasicWithCustomProvider::new(BasicIdentityProvider),
group_context_extensions: Default::default(),
user_rules: pass_through_rules(),
with_psk_storage: AlwaysFoundPskStorage,
cipher_suite_provider,
}
}
}
impl<'a, C, F, P, CSP> CommitReceiver<'a, C, F, P, CSP>
where
C: IdentityProvider,
F: MlsRules,
P: PreSharedKeyStorage,
CSP: CipherSuiteProvider,
{
#[cfg(feature = "by_ref_proposal")]
pub fn with_identity_provider<V>(self, validator: V) -> CommitReceiver<'a, V, F, P, CSP>
where
V: IdentityProvider,
{
CommitReceiver {
tree: self.tree,
sender: self.sender,
receiver: self.receiver,
cache: self.cache,
identity_provider: validator,
group_context_extensions: self.group_context_extensions,
user_rules: self.user_rules,
with_psk_storage: self.with_psk_storage,
cipher_suite_provider: self.cipher_suite_provider,
}
}
pub fn with_user_rules<G>(self, f: G) -> CommitReceiver<'a, C, G, P, CSP>
where
G: MlsRules,
{
CommitReceiver {
tree: self.tree,
sender: self.sender,
receiver: self.receiver,
cache: self.cache,
identity_provider: self.identity_provider,
group_context_extensions: self.group_context_extensions,
user_rules: f,
with_psk_storage: self.with_psk_storage,
cipher_suite_provider: self.cipher_suite_provider,
}
}
pub fn with_psk_storage<V>(self, v: V) -> CommitReceiver<'a, C, F, V, CSP>
where
V: PreSharedKeyStorage,
{
CommitReceiver {
tree: self.tree,
sender: self.sender,
receiver: self.receiver,
cache: self.cache,
identity_provider: self.identity_provider,
group_context_extensions: self.group_context_extensions,
user_rules: self.user_rules,
with_psk_storage: v,
cipher_suite_provider: self.cipher_suite_provider,
}
}
#[cfg(feature = "by_ref_proposal")]
pub fn with_extensions(self, extensions: ExtensionList) -> Self {
Self {
group_context_extensions: extensions,
..self
}
}
pub fn cache<S>(mut self, r: ProposalRef, p: Proposal, proposer: S) -> Self
where
S: Into<Sender>,
{
self.cache.insert(r, p, proposer.into());
self
}
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
pub async fn receive<I>(&self, proposals: I) -> Result<ProvisionalState, MlsError>
where
I: IntoIterator,
I::Item: Into<ProposalOrRef>,
{
self.cache
.resolve_for_commit_default(
self.sender,
proposals.into_iter().map(Into::into).collect(),
None,
&self.group_context_extensions,
&self.identity_provider,
&self.cipher_suite_provider,
self.tree,
&self.with_psk_storage,
&self.user_rules,
)
.await
}
}
pub(crate) fn make_proposal_cache() -> ProposalCache {
ProposalCache::new(TEST_PROTOCOL_VERSION, TEST_GROUP.to_vec())
}
pub fn pass_through_rules() -> DefaultMlsRules {
DefaultMlsRules::new()
}
impl ProposalCache {
#[allow(clippy::too_many_arguments)]
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
pub async fn 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,
{
let mut context =
get_test_group_context(123, cipher_suite_provider.cipher_suite()).await;
context.extensions = group_extensions.clone();
let mut state = GroupState::new(
context,
public_tree.clone(),
Vec::new().into(),
ConfirmationTag::empty(cipher_suite_provider).await,
);
state.proposals.proposals.clone_from(&self.proposals);
let proposals = self.resolve_for_commit(sender, proposal_list)?;
state
.apply_resolved(
sender,
proposals,
external_leaf,
identity_provider,
cipher_suite_provider,
psk_storage,
&user_rules,
None,
CommitDirection::Receive,
)
.await
}
#[allow(clippy::too_many_arguments)]
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
pub async fn 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,
{
let state = GroupState::new(
context.clone(),
public_tree.clone(),
Vec::new().into(),
ConfirmationTag::empty(cipher_suite_provider).await,
);
let proposals = self.prepare_commit(sender, additional_proposals);
state
.apply_resolved(
sender,
proposals,
external_leaf,
identity_provider,
cipher_suite_provider,
psk_storage,
&user_rules,
None,
CommitDirection::Send,
)
.await
}
}
}
// TODO add tests for lite version of filtering
#[cfg(all(feature = "by_ref_proposal", test))]
mod tests {
use alloc::{boxed::Box, vec, vec::Vec};
use super::test_utils::{make_proposal_cache, pass_through_rules, CommitReceiver};
use super::{CachedProposal, ProposalCache};
use crate::client::MlsError;
use crate::group::message_processor::ProvisionalState;
use crate::group::mls_rules::{CommitDirection, CommitSource, EncryptionOptions};
use crate::group::proposal_filter::{ProposalBundle, ProposalInfo, ProposalSource};
use crate::group::proposal_ref::test_utils::auth_content_from_proposal;
use crate::group::proposal_ref::ProposalRef;
use crate::group::{
AddProposal, AuthenticatedContent, Content, ExternalInit, Proposal, ProposalOrRef,
ReInitProposal, RemoveProposal, Roster, Sender, UpdateProposal,
};
use crate::key_package::test_utils::test_key_package_with_signer;
use crate::signer::Signable;
use crate::tree_kem::leaf_node::LeafNode;
use crate::tree_kem::node::LeafIndex;
use crate::tree_kem::TreeKemPublic;
use crate::{
client::test_utils::{TEST_CIPHER_SUITE, TEST_PROTOCOL_VERSION},
crypto::{self, test_utils::test_cipher_suite_provider},
extension::test_utils::TestExtension,
group::{
message_processor::path_update_required,
proposal_filter::proposer_can_propose,
test_utils::{get_test_group_context, random_bytes, test_group, TEST_GROUP},
},
identity::basic::BasicIdentityProvider,
identity::test_utils::{get_test_signing_identity, BasicWithCustomProvider},
key_package::{test_utils::test_key_package, KeyPackageGenerator},
mls_rules::{CommitOptions, DefaultMlsRules},
psk::AlwaysFoundPskStorage,
tree_kem::{
leaf_node::{
test_utils::{
default_properties, get_basic_test_node, get_basic_test_node_capabilities,
get_basic_test_node_sig_key, get_test_capabilities,
},
ConfigProperties, LeafNodeSigningContext, LeafNodeSource,
},
Lifetime,
},
};
use crate::{KeyPackage, MlsRules};
use crate::extension::RequiredCapabilitiesExt;
#[cfg(feature = "by_ref_proposal")]
use crate::{
extension::ExternalSendersExt,
tree_kem::leaf_node_validator::test_utils::FailureIdentityProvider,
};
#[cfg(feature = "psk")]
use crate::{
group::proposal::PreSharedKeyProposal,
psk::{
ExternalPskId, JustPreSharedKeyID, PreSharedKeyID, PskGroupId, PskNonce,
ResumptionPSKUsage, ResumptionPsk,
},
};
#[cfg(feature = "custom_proposal")]
use crate::group::proposal::CustomProposal;
use assert_matches::assert_matches;
use core::convert::Infallible;
use itertools::Itertools;
use mls_rs_core::crypto::{CipherSuite, CipherSuiteProvider};
use mls_rs_core::extension::ExtensionList;
use mls_rs_core::group::{Capabilities, ProposalType};
use mls_rs_core::identity::IdentityProvider;
use mls_rs_core::protocol_version::ProtocolVersion;
use mls_rs_core::psk::{PreSharedKey, PreSharedKeyStorage};
use mls_rs_core::{
extension::MlsExtension,
identity::{Credential, CredentialType, CustomCredential},
};
fn test_sender() -> u32 {
1
}
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
async fn new_tree_custom_proposals(
name: &str,
proposal_types: Vec<ProposalType>,
) -> (LeafIndex, TreeKemPublic) {
let (leaf, secret, _) = get_basic_test_node_capabilities(
TEST_CIPHER_SUITE,
name,
Capabilities {
proposals: proposal_types,
..get_test_capabilities()
},
)
.await;
let (pub_tree, priv_tree) =
TreeKemPublic::derive(leaf, secret, &BasicIdentityProvider, &Default::default())
.await
.unwrap();
(priv_tree.self_index, pub_tree)
}
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
async fn new_tree(name: &str) -> (LeafIndex, TreeKemPublic) {
new_tree_custom_proposals(name, vec![]).await
}
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
async fn add_member(tree: &mut TreeKemPublic, name: &str) -> LeafIndex {
let test_node = get_basic_test_node(TEST_CIPHER_SUITE, name).await;
tree.add_leaves(
vec![test_node],
&BasicIdentityProvider,
&test_cipher_suite_provider(TEST_CIPHER_SUITE),
)
.await
.unwrap()[0]
}
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
async fn update_leaf_node(name: &str, leaf_index: u32) -> LeafNode {
let (mut leaf, _, signer) = get_basic_test_node_sig_key(TEST_CIPHER_SUITE, name).await;
leaf.update(
&test_cipher_suite_provider(TEST_CIPHER_SUITE),
TEST_GROUP,
leaf_index,
default_properties(),
None,
&signer,
)
.await
.unwrap();
leaf
}
struct TestProposals {
test_sender: u32,
test_proposals: Vec<AuthenticatedContent>,
expected_effects: ProvisionalState,
tree: TreeKemPublic,
}
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
async fn test_proposals(
protocol_version: ProtocolVersion,
cipher_suite: CipherSuite,
) -> TestProposals {
let cipher_suite_provider = test_cipher_suite_provider(cipher_suite);
let (sender_leaf, sender_leaf_secret, _) =
get_basic_test_node_sig_key(cipher_suite, "alice").await;
let sender = LeafIndex(0);
let (mut tree, _) = TreeKemPublic::derive(
sender_leaf,
sender_leaf_secret,
&BasicIdentityProvider,
&Default::default(),
)
.await
.unwrap();
let add_package = test_key_package(protocol_version, cipher_suite, "dave").await;
let remove_leaf_index = add_member(&mut tree, "carol").await;
let add = Proposal::Add(Box::new(AddProposal {
key_package: add_package.clone(),
}));
let remove = Proposal::Remove(RemoveProposal {
to_remove: remove_leaf_index,
});
let extensions = Proposal::GroupContextExtensions(ExtensionList::new());
let proposals = vec![add, remove, extensions];
let test_node = get_basic_test_node(cipher_suite, "charlie").await;
let test_sender = *tree
.add_leaves(
vec![test_node],
&BasicIdentityProvider,
&cipher_suite_provider,
)
.await
.unwrap()[0];
let mut expected_tree = tree.clone();
let mut bundle = ProposalBundle::default();
let plaintext = proposals
.iter()
.cloned()
.map(|p| auth_content_from_proposal(p, sender))
.collect_vec();
for i in 0..proposals.len() {
let pref = ProposalRef::from_content(&cipher_suite_provider, &plaintext[i])
.await
.unwrap();
bundle.add(
proposals[i].clone(),
Sender::Member(test_sender),
ProposalSource::ByReference(pref),
)
}
expected_tree
.batch_edit(
&mut bundle,
&Default::default(),
&BasicIdentityProvider,
&cipher_suite_provider,
true,
)
.await
.unwrap();
let expected_effects = ProvisionalState {
public_tree: expected_tree,
group_context: get_test_group_context(1, cipher_suite).await,
external_init_index: None,
indexes_of_added_kpkgs: vec![LeafIndex(1)],
#[cfg(feature = "state_update")]
unused_proposals: vec![],
applied_proposals: bundle,
};
TestProposals {
test_sender,
test_proposals: plaintext,
expected_effects,
tree,
}
}
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
async fn filter_proposals(
cipher_suite: CipherSuite,
proposals: Vec<AuthenticatedContent>,
) -> Vec<(ProposalRef, CachedProposal)> {
let mut contents = Vec::new();
for p in proposals {
if let Content::Proposal(proposal) = &p.content.content {
let proposal_ref =
ProposalRef::from_content(&test_cipher_suite_provider(cipher_suite), &p)
.await
.unwrap();
contents.push((
proposal_ref,
CachedProposal::new(proposal.as_ref().clone(), p.content.sender),
));
}
}
contents
}
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
async fn make_proposal_ref<S>(p: &Proposal, sender: S) -> ProposalRef
where
S: Into<Sender>,
{
ProposalRef::from_content(
&test_cipher_suite_provider(TEST_CIPHER_SUITE),
&auth_content_from_proposal(p.clone(), sender),
)
.await
.unwrap()
}
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
async fn make_proposal_info<S>(p: &Proposal, sender: S) -> ProposalInfo<Proposal>
where
S: Into<Sender> + Clone,
{
ProposalInfo {
proposal: p.clone(),
sender: sender.clone().into(),
source: ProposalSource::ByReference(make_proposal_ref(p, sender).await),
}
}
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
async fn test_proposal_cache_setup(proposals: Vec<AuthenticatedContent>) -> ProposalCache {
let mut cache = make_proposal_cache();
cache.extend(filter_proposals(TEST_CIPHER_SUITE, proposals).await);
cache
}
fn assert_matches(mut expected_state: ProvisionalState, state: ProvisionalState) {
let expected_proposals = expected_state.applied_proposals.into_proposals_or_refs();
let proposals = state.applied_proposals.into_proposals_or_refs();
assert_eq!(proposals.len(), expected_proposals.len());
// Determine there are no duplicates in the proposals returned
assert!(!proposals.iter().enumerate().any(|(i, p1)| proposals
.iter()
.enumerate()
.any(|(j, p2)| p1 == p2 && i != j)),);
// Proposal order may change so we just compare the length and contents are the same
expected_proposals
.iter()
.for_each(|p| assert!(proposals.contains(p)));
assert_eq!(
expected_state.external_init_index,
state.external_init_index
);
// We don't compare the epoch in this test.
expected_state.group_context.epoch = state.group_context.epoch;
assert_eq!(expected_state.group_context, state.group_context);
assert_eq!(
expected_state.indexes_of_added_kpkgs,
state.indexes_of_added_kpkgs
);
assert_eq!(expected_state.public_tree, state.public_tree);
#[cfg(feature = "state_update")]
assert_eq!(expected_state.unused_proposals, state.unused_proposals);
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn test_proposal_cache_commit_all_cached() {
let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
let TestProposals {
test_sender,
test_proposals,
expected_effects,
tree,
..
} = test_proposals(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
let cache = test_proposal_cache_setup(test_proposals.clone()).await;
let provisional_state = cache
.prepare_commit_default(
Sender::Member(test_sender),
vec![],
&get_test_group_context(0, TEST_CIPHER_SUITE).await,
&BasicIdentityProvider,
&cipher_suite_provider,
&tree,
None,
&AlwaysFoundPskStorage,
pass_through_rules(),
)
.await
.unwrap();
assert_matches(expected_effects, provisional_state)
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn test_proposal_cache_commit_additional() {
let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
let TestProposals {
test_sender,
test_proposals,
mut expected_effects,
tree,
..
} = test_proposals(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
let additional_key_package =
test_key_package(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "frank").await;
let additional = AddProposal {
key_package: additional_key_package.clone(),
};
let cache = test_proposal_cache_setup(test_proposals.clone()).await;
let provisional_state = cache
.prepare_commit_default(
Sender::Member(test_sender),
vec![Proposal::Add(Box::new(additional.clone()))],
&get_test_group_context(0, TEST_CIPHER_SUITE).await,
&BasicIdentityProvider,
&cipher_suite_provider,
&tree,
None,
&AlwaysFoundPskStorage,
pass_through_rules(),
)
.await
.unwrap();
expected_effects.applied_proposals.add(
Proposal::Add(Box::new(additional.clone())),
Sender::Member(test_sender),
ProposalSource::ByValue,
);
let leaf = vec![additional_key_package.leaf_node.clone()];
expected_effects
.public_tree
.add_leaves(leaf, &BasicIdentityProvider, &cipher_suite_provider)
.await
.unwrap();
expected_effects.indexes_of_added_kpkgs.push(LeafIndex(3));
assert_matches(expected_effects, provisional_state);
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn test_proposal_cache_update_filter() {
let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
let TestProposals {
test_proposals,
tree,
..
} = test_proposals(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
let update_proposal = make_update_proposal("foo").await;
let additional = vec![Proposal::Update(update_proposal)];
let cache = test_proposal_cache_setup(test_proposals).await;
let res = cache
.prepare_commit_default(
Sender::Member(test_sender()),
additional,
&get_test_group_context(0, TEST_CIPHER_SUITE).await,
&BasicIdentityProvider,
&cipher_suite_provider,
&tree,
None,
&AlwaysFoundPskStorage,
pass_through_rules(),
)
.await;
assert_matches!(res, Err(MlsError::InvalidProposalTypeForSender));
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn test_proposal_cache_removal_override_update() {
let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
let TestProposals {
test_sender,
test_proposals,
tree,
..
} = test_proposals(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
let update = Proposal::Update(make_update_proposal("foo").await);
let update_proposal_ref = make_proposal_ref(&update, LeafIndex(1)).await;
let mut cache = test_proposal_cache_setup(test_proposals).await;
cache.insert(update_proposal_ref.clone(), update, Sender::Member(1));
let provisional_state = cache
.prepare_commit_default(
Sender::Member(test_sender),
vec![],
&get_test_group_context(0, TEST_CIPHER_SUITE).await,
&BasicIdentityProvider,
&cipher_suite_provider,
&tree,
None,
&AlwaysFoundPskStorage,
pass_through_rules(),
)
.await
.unwrap();
assert!(provisional_state
.applied_proposals
.removals
.iter()
.any(|p| *p.proposal.to_remove == 1));
assert!(!provisional_state
.applied_proposals
.into_proposals_or_refs()
.contains(&ProposalOrRef::Reference(update_proposal_ref)))
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn test_proposal_cache_filter_duplicates_insert() {
let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
let TestProposals {
test_sender,
test_proposals,
expected_effects,
tree,
..
} = test_proposals(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
let mut cache = test_proposal_cache_setup(test_proposals.clone()).await;
cache.extend(filter_proposals(TEST_CIPHER_SUITE, test_proposals.clone()).await);
let provisional_state = cache
.prepare_commit_default(
Sender::Member(test_sender),
vec![],
&get_test_group_context(0, TEST_CIPHER_SUITE).await,
&BasicIdentityProvider,
&cipher_suite_provider,
&tree,
None,
&AlwaysFoundPskStorage,
pass_through_rules(),
)
.await
.unwrap();
assert_matches(expected_effects, provisional_state)
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn test_proposal_cache_filter_duplicates_additional() {
let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
let TestProposals {
test_proposals,
expected_effects,
tree,
..
} = test_proposals(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
let mut cache = test_proposal_cache_setup(test_proposals.clone()).await;
// Updates from different senders will be allowed so we test duplicates for add / remove
let additional = test_proposals
.clone()
.into_iter()
.filter_map(|plaintext| match plaintext.content.content {
Content::Proposal(p) if p.proposal_type() == ProposalType::UPDATE => None,
Content::Proposal(_) => Some(plaintext),
_ => None,
})
.collect::<Vec<_>>();
cache.extend(filter_proposals(TEST_CIPHER_SUITE, additional).await);
let provisional_state = cache
.prepare_commit_default(
Sender::Member(2),
Vec::new(),
&get_test_group_context(0, TEST_CIPHER_SUITE).await,
&BasicIdentityProvider,
&cipher_suite_provider,
&tree,
None,
&AlwaysFoundPskStorage,
pass_through_rules(),
)
.await
.unwrap();
assert_matches(expected_effects, provisional_state)
}
#[cfg(feature = "private_message")]
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn test_proposal_cache_is_empty() {
let mut cache = make_proposal_cache();
assert!(cache.is_empty());
let test_proposal = Proposal::Remove(RemoveProposal {
to_remove: LeafIndex(test_sender()),
});
let proposer = test_sender();
let test_proposal_ref = make_proposal_ref(&test_proposal, LeafIndex(proposer)).await;
cache.insert(test_proposal_ref, test_proposal, Sender::Member(proposer));
assert!(!cache.is_empty())
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn test_proposal_cache_resolve() {
let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
let TestProposals {
test_sender,
test_proposals,
tree,
..
} = test_proposals(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
let cache = test_proposal_cache_setup(test_proposals).await;
let proposal = Proposal::Add(Box::new(AddProposal {
key_package: test_key_package(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "frank").await,
}));
let additional = vec![proposal];
let expected_effects = cache
.prepare_commit_default(
Sender::Member(test_sender),
additional,
&get_test_group_context(0, TEST_CIPHER_SUITE).await,
&BasicIdentityProvider,
&cipher_suite_provider,
&tree,
None,
&AlwaysFoundPskStorage,
pass_through_rules(),
)
.await
.unwrap();
let proposals = expected_effects
.applied_proposals
.clone()
.into_proposals_or_refs();
let resolution = cache
.resolve_for_commit_default(
Sender::Member(test_sender),
proposals,
None,
&ExtensionList::new(),
&BasicIdentityProvider,
&cipher_suite_provider,
&tree,
&AlwaysFoundPskStorage,
pass_through_rules(),
)
.await
.unwrap();
assert_matches(expected_effects, resolution);
}
#[cfg(feature = "psk")]
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn proposal_cache_filters_duplicate_psk_ids() {
let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
let (alice, tree) = new_tree("alice").await;
let cache = make_proposal_cache();
let proposal = Proposal::Psk(make_external_psk(
b"ted",
crate::psk::PskNonce::random(&test_cipher_suite_provider(TEST_CIPHER_SUITE)).unwrap(),
));
let res = cache
.prepare_commit_default(
Sender::Member(*alice),
vec![proposal.clone(), proposal],
&get_test_group_context(0, TEST_CIPHER_SUITE).await,
&BasicIdentityProvider,
&cipher_suite_provider,
&tree,
None,
&AlwaysFoundPskStorage,
pass_through_rules(),
)
.await;
assert_matches!(res, Err(MlsError::DuplicatePskIds));
}
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
async fn test_node() -> LeafNode {
let (mut leaf_node, _, signer) =
get_basic_test_node_sig_key(TEST_CIPHER_SUITE, "foo").await;
leaf_node
.commit(
&test_cipher_suite_provider(TEST_CIPHER_SUITE),
TEST_GROUP,
0,
default_properties(),
None,
&signer,
)
.await
.unwrap();
leaf_node
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn external_commit_must_have_new_leaf() {
let cache = make_proposal_cache();
let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
let kem_output = vec![0; cipher_suite_provider.kdf_extract_size()];
let group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
let public_tree = &group.group.state.public_tree;
let res = cache
.resolve_for_commit_default(
Sender::NewMemberCommit,
vec![ProposalOrRef::Proposal(Box::new(Proposal::ExternalInit(
ExternalInit { kem_output },
)))],
None,
&group.group.context().extensions,
&BasicIdentityProvider,
&cipher_suite_provider,
public_tree,
&AlwaysFoundPskStorage,
pass_through_rules(),
)
.await;
assert_matches!(res, Err(MlsError::ExternalCommitMustHaveNewLeaf));
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn proposal_cache_rejects_proposals_by_ref_for_new_member() {
let mut cache = make_proposal_cache();
let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
let proposal = {
let kem_output = vec![0; cipher_suite_provider.kdf_extract_size()];
Proposal::ExternalInit(ExternalInit { kem_output })
};
let proposal_ref = make_proposal_ref(&proposal, test_sender()).await;
cache.insert(
proposal_ref.clone(),
proposal,
Sender::Member(test_sender()),
);
let group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
let public_tree = &group.group.state.public_tree;
let res = cache
.resolve_for_commit_default(
Sender::NewMemberCommit,
vec![ProposalOrRef::Reference(proposal_ref)],
Some(&test_node().await),
&group.group.context().extensions,
&BasicIdentityProvider,
&cipher_suite_provider,
public_tree,
&AlwaysFoundPskStorage,
pass_through_rules(),
)
.await;
assert_matches!(res, Err(MlsError::OnlyMembersCanCommitProposalsByRef));
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn proposal_cache_rejects_multiple_external_init_proposals_in_commit() {
let cache = make_proposal_cache();
let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
let kem_output = vec![0; cipher_suite_provider.kdf_extract_size()];
let group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
let public_tree = &group.group.state.public_tree;
let res = cache
.resolve_for_commit_default(
Sender::NewMemberCommit,
[
Proposal::ExternalInit(ExternalInit {
kem_output: kem_output.clone(),
}),
Proposal::ExternalInit(ExternalInit { kem_output }),
]
.into_iter()
.map(|p| ProposalOrRef::Proposal(Box::new(p)))
.collect(),
Some(&test_node().await),
&group.group.context().extensions,
&BasicIdentityProvider,
&cipher_suite_provider,
public_tree,
&AlwaysFoundPskStorage,
pass_through_rules(),
)
.await;
assert_matches!(
res,
Err(MlsError::ExternalCommitMustHaveExactlyOneExternalInit)
);
}
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
async fn new_member_commits_proposal(proposal: Proposal) -> Result<ProvisionalState, MlsError> {
let cache = make_proposal_cache();
let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
let kem_output = vec![0; cipher_suite_provider.kdf_extract_size()];
let group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
let public_tree = &group.group.state.public_tree;
cache
.resolve_for_commit_default(
Sender::NewMemberCommit,
[
Proposal::ExternalInit(ExternalInit { kem_output }),
proposal,
]
.into_iter()
.map(|p| ProposalOrRef::Proposal(Box::new(p)))
.collect(),
Some(&test_node().await),
&group.group.context().extensions,
&BasicIdentityProvider,
&cipher_suite_provider,
public_tree,
&AlwaysFoundPskStorage,
pass_through_rules(),
)
.await
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn new_member_cannot_commit_add_proposal() {
let res = new_member_commits_proposal(Proposal::Add(Box::new(AddProposal {
key_package: test_key_package(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "frank").await,
})))
.await;
assert_matches!(
res,
Err(MlsError::InvalidProposalTypeInExternalCommit(
ProposalType::ADD
))
);
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn new_member_cannot_commit_more_than_one_remove_proposal() {
let cache = make_proposal_cache();
let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
let kem_output = vec![0; cipher_suite_provider.kdf_extract_size()];
let group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
let group_extensions = group.group.context().extensions.clone();
let mut public_tree = group.group.state.public_tree;
let foo = get_basic_test_node(TEST_CIPHER_SUITE, "foo").await;
let bar = get_basic_test_node(TEST_CIPHER_SUITE, "bar").await;
let test_leaf_nodes = vec![foo, bar];
let test_leaf_node_indexes = public_tree
.add_leaves(
test_leaf_nodes,
&BasicIdentityProvider,
&cipher_suite_provider,
)
.await
.unwrap();
let proposals = vec![
Proposal::ExternalInit(ExternalInit { kem_output }),
Proposal::Remove(RemoveProposal {
to_remove: test_leaf_node_indexes[0],
}),
Proposal::Remove(RemoveProposal {
to_remove: test_leaf_node_indexes[1],
}),
];
let res = cache
.resolve_for_commit_default(
Sender::NewMemberCommit,
proposals
.into_iter()
.map(|p| ProposalOrRef::Proposal(Box::new(p)))
.collect(),
Some(&test_node().await),
&group_extensions,
&BasicIdentityProvider,
&cipher_suite_provider,
&public_tree,
&AlwaysFoundPskStorage,
pass_through_rules(),
)
.await;
assert_matches!(res, Err(MlsError::ExternalCommitWithMoreThanOneRemove));
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn new_member_remove_proposal_invalid_credential() {
let cache = make_proposal_cache();
let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
let kem_output = vec![0; cipher_suite_provider.kdf_extract_size()];
let group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
let group_extensions = group.group.context().extensions.clone();
let mut public_tree = group.group.state.public_tree;
let node = get_basic_test_node(TEST_CIPHER_SUITE, "bar").await;
let test_leaf_nodes = vec![node];
let test_leaf_node_indexes = public_tree
.add_leaves(
test_leaf_nodes,
&BasicIdentityProvider,
&cipher_suite_provider,
)
.await
.unwrap();
let proposals = vec![
Proposal::ExternalInit(ExternalInit { kem_output }),
Proposal::Remove(RemoveProposal {
to_remove: test_leaf_node_indexes[0],
}),
];
let res = cache
.resolve_for_commit_default(
Sender::NewMemberCommit,
proposals
.into_iter()
.map(|p| ProposalOrRef::Proposal(Box::new(p)))
.collect(),
Some(&test_node().await),
&group_extensions,
&BasicIdentityProvider,
&cipher_suite_provider,
&public_tree,
&AlwaysFoundPskStorage,
pass_through_rules(),
)
.await;
assert_matches!(res, Err(MlsError::ExternalCommitRemovesOtherIdentity));
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn new_member_remove_proposal_valid_credential() {
let cache = make_proposal_cache();
let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
let kem_output = vec![0; cipher_suite_provider.kdf_extract_size()];
let group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
let group_extensions = group.group.context().extensions.clone();
let mut public_tree = group.group.state.public_tree;
let node = get_basic_test_node(TEST_CIPHER_SUITE, "foo").await;
let test_leaf_nodes = vec![node];
let test_leaf_node_indexes = public_tree
.add_leaves(
test_leaf_nodes,
&BasicIdentityProvider,
&cipher_suite_provider,
)
.await
.unwrap();
let proposals = vec![
Proposal::ExternalInit(ExternalInit { kem_output }),
Proposal::Remove(RemoveProposal {
to_remove: test_leaf_node_indexes[0],
}),
];
let res = cache
.resolve_for_commit_default(
Sender::NewMemberCommit,
proposals
.into_iter()
.map(|p| ProposalOrRef::Proposal(Box::new(p)))
.collect(),
Some(&test_node().await),
&group_extensions,
&BasicIdentityProvider,
&cipher_suite_provider,
&public_tree,
&AlwaysFoundPskStorage,
pass_through_rules(),
)
.await;
assert_matches!(res, Ok(_));
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn new_member_cannot_commit_update_proposal() {
let res = new_member_commits_proposal(Proposal::Update(UpdateProposal {
leaf_node: get_basic_test_node(TEST_CIPHER_SUITE, "foo").await,
}))
.await;
assert_matches!(
res,
Err(MlsError::InvalidProposalTypeInExternalCommit(
ProposalType::UPDATE
))
);
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn new_member_cannot_commit_group_extensions_proposal() {
let res =
new_member_commits_proposal(Proposal::GroupContextExtensions(ExtensionList::new()))
.await;
assert_matches!(
res,
Err(MlsError::InvalidProposalTypeInExternalCommit(
ProposalType::GROUP_CONTEXT_EXTENSIONS,
))
);
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn new_member_cannot_commit_reinit_proposal() {
let res = new_member_commits_proposal(Proposal::ReInit(ReInitProposal {
group_id: b"foo".to_vec(),
version: TEST_PROTOCOL_VERSION,
cipher_suite: TEST_CIPHER_SUITE,
extensions: ExtensionList::new(),
}))
.await;
assert_matches!(
res,
Err(MlsError::InvalidProposalTypeInExternalCommit(
ProposalType::RE_INIT
))
);
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn new_member_commit_must_contain_an_external_init_proposal() {
let cache = make_proposal_cache();
let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
let group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
let public_tree = &group.group.state.public_tree;
let res = cache
.resolve_for_commit_default(
Sender::NewMemberCommit,
Vec::new(),
Some(&test_node().await),
&group.group.context().extensions,
&BasicIdentityProvider,
&cipher_suite_provider,
public_tree,
&AlwaysFoundPskStorage,
pass_through_rules(),
)
.await;
assert_matches!(
res,
Err(MlsError::ExternalCommitMustHaveExactlyOneExternalInit)
);
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn test_path_update_required_empty() {
let cache = make_proposal_cache();
let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
let mut tree = TreeKemPublic::new();
add_member(&mut tree, "alice").await;
add_member(&mut tree, "bob").await;
let effects = cache
.prepare_commit_default(
Sender::Member(test_sender()),
vec![],
&get_test_group_context(1, TEST_CIPHER_SUITE).await,
&BasicIdentityProvider,
&cipher_suite_provider,
&tree,
None,
&AlwaysFoundPskStorage,
pass_through_rules(),
)
.await
.unwrap();
assert!(path_update_required(&effects.applied_proposals))
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn test_path_update_required_updates() {
let mut cache = make_proposal_cache();
let update = Proposal::Update(make_update_proposal("bar").await);
let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
cache.insert(
make_proposal_ref(&update, LeafIndex(2)).await,
update,
Sender::Member(2),
);
let mut tree = TreeKemPublic::new();
add_member(&mut tree, "alice").await;
add_member(&mut tree, "bob").await;
add_member(&mut tree, "carol").await;
let effects = cache
.prepare_commit_default(
Sender::Member(test_sender()),
Vec::new(),
&get_test_group_context(1, TEST_CIPHER_SUITE).await,
&BasicIdentityProvider,
&cipher_suite_provider,
&tree,
None,
&AlwaysFoundPskStorage,
pass_through_rules(),
)
.await
.unwrap();
assert!(path_update_required(&effects.applied_proposals))
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn test_path_update_required_removes() {
let cache = make_proposal_cache();
let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
let (alice_leaf, alice_secret, _) =
get_basic_test_node_sig_key(TEST_CIPHER_SUITE, "alice").await;
let alice = 0;
let (mut tree, _) = TreeKemPublic::derive(
alice_leaf,
alice_secret,
&BasicIdentityProvider,
&Default::default(),
)
.await
.unwrap();
let bob_node = get_basic_test_node(TEST_CIPHER_SUITE, "bob").await;
let bob = tree
.add_leaves(
vec![bob_node],
&BasicIdentityProvider,
&cipher_suite_provider,
)
.await
.unwrap()[0];
let remove = Proposal::Remove(RemoveProposal { to_remove: bob });
let effects = cache
.prepare_commit_default(
Sender::Member(alice),
vec![remove],
&get_test_group_context(1, TEST_CIPHER_SUITE).await,
&BasicIdentityProvider,
&cipher_suite_provider,
&tree,
None,
&AlwaysFoundPskStorage,
pass_through_rules(),
)
.await
.unwrap();
assert!(path_update_required(&effects.applied_proposals))
}
#[cfg(feature = "psk")]
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn test_path_update_not_required() {
let (alice, tree) = new_tree("alice").await;
let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
let cache = make_proposal_cache();
let psk = Proposal::Psk(PreSharedKeyProposal {
psk: PreSharedKeyID::new(
JustPreSharedKeyID::External(ExternalPskId::new(vec![])),
&test_cipher_suite_provider(TEST_CIPHER_SUITE),
)
.unwrap(),
});
let add = Proposal::Add(Box::new(AddProposal {
key_package: test_key_package(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "bob").await,
}));
let effects = cache
.prepare_commit_default(
Sender::Member(*alice),
vec![psk, add],
&get_test_group_context(1, TEST_CIPHER_SUITE).await,
&BasicIdentityProvider,
&cipher_suite_provider,
&tree,
None,
&AlwaysFoundPskStorage,
pass_through_rules(),
)
.await
.unwrap();
assert!(!path_update_required(&effects.applied_proposals))
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn path_update_is_not_required_for_re_init() {
let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
let (alice, tree) = new_tree("alice").await;
let cache = make_proposal_cache();
let reinit = Proposal::ReInit(ReInitProposal {
group_id: vec![],
version: TEST_PROTOCOL_VERSION,
cipher_suite: TEST_CIPHER_SUITE,
extensions: Default::default(),
});
let effects = cache
.prepare_commit_default(
Sender::Member(*alice),
vec![reinit],
&get_test_group_context(1, TEST_CIPHER_SUITE).await,
&BasicIdentityProvider,
&cipher_suite_provider,
&tree,
None,
&AlwaysFoundPskStorage,
pass_through_rules(),
)
.await
.unwrap();
assert!(!path_update_required(&effects.applied_proposals))
}
#[derive(Debug)]
struct CommitSender<'a, C, F, P, CSP> {
cipher_suite_provider: CSP,
tree: &'a TreeKemPublic,
sender: LeafIndex,
cache: ProposalCache,
additional_proposals: Vec<Proposal>,
identity_provider: C,
user_rules: F,
psk_storage: P,
}
impl<'a, CSP>
CommitSender<'a, BasicWithCustomProvider, DefaultMlsRules, AlwaysFoundPskStorage, CSP>
{
fn new(tree: &'a TreeKemPublic, sender: LeafIndex, cipher_suite_provider: CSP) -> Self {
Self {
tree,
sender,
cache: make_proposal_cache(),
additional_proposals: Vec::new(),
identity_provider: BasicWithCustomProvider::new(BasicIdentityProvider::new()),
user_rules: pass_through_rules(),
psk_storage: AlwaysFoundPskStorage,
cipher_suite_provider,
}
}
}
impl<'a, C, F, P, CSP> CommitSender<'a, C, F, P, CSP>
where
C: IdentityProvider,
F: MlsRules,
P: PreSharedKeyStorage,
CSP: CipherSuiteProvider,
{
#[cfg(feature = "by_ref_proposal")]
fn with_identity_provider<V>(self, identity_provider: V) -> CommitSender<'a, V, F, P, CSP>
where
V: IdentityProvider,
{
CommitSender {
identity_provider,
cipher_suite_provider: self.cipher_suite_provider,
tree: self.tree,
sender: self.sender,
cache: self.cache,
additional_proposals: self.additional_proposals,
user_rules: self.user_rules,
psk_storage: self.psk_storage,
}
}
fn cache<S>(mut self, r: ProposalRef, p: Proposal, proposer: S) -> Self
where
S: Into<Sender>,
{
self.cache.insert(r, p, proposer.into());
self
}
fn with_additional<I>(mut self, proposals: I) -> Self
where
I: IntoIterator<Item = Proposal>,
{
self.additional_proposals.extend(proposals);
self
}
fn with_user_rules<G>(self, f: G) -> CommitSender<'a, C, G, P, CSP>
where
G: MlsRules,
{
CommitSender {
tree: self.tree,
sender: self.sender,
cache: self.cache,
additional_proposals: self.additional_proposals,
identity_provider: self.identity_provider,
user_rules: f,
psk_storage: self.psk_storage,
cipher_suite_provider: self.cipher_suite_provider,
}
}
fn with_psk_storage<V>(self, v: V) -> CommitSender<'a, C, F, V, CSP>
where
V: PreSharedKeyStorage,
{
CommitSender {
tree: self.tree,
sender: self.sender,
cache: self.cache,
additional_proposals: self.additional_proposals,
identity_provider: self.identity_provider,
user_rules: self.user_rules,
psk_storage: v,
cipher_suite_provider: self.cipher_suite_provider,
}
}
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
async fn send(&self) -> Result<(Vec<ProposalOrRef>, ProvisionalState), MlsError> {
let state = self
.cache
.prepare_commit_default(
Sender::Member(*self.sender),
self.additional_proposals.clone(),
&get_test_group_context(1, TEST_CIPHER_SUITE).await,
&self.identity_provider,
&self.cipher_suite_provider,
self.tree,
None,
&self.psk_storage,
&self.user_rules,
)
.await?;
let proposals = state.applied_proposals.clone().into_proposals_or_refs();
Ok((proposals, state))
}
}
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
async fn key_package_with_invalid_signature() -> KeyPackage {
let mut kp = test_key_package(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "mallory").await;
kp.signature.clear();
kp
}
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
async fn key_package_with_public_key(key: crypto::HpkePublicKey) -> KeyPackage {
let cs = test_cipher_suite_provider(TEST_CIPHER_SUITE);
let (mut key_package, signer) =
test_key_package_with_signer(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "test").await;
key_package.leaf_node.public_key = key;
key_package
.leaf_node
.sign(
&cs,
&signer,
&LeafNodeSigningContext {
group_id: None,
leaf_index: None,
},
)
.await
.unwrap();
key_package.sign(&cs, &signer, &()).await.unwrap();
key_package
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn receiving_add_with_invalid_key_package_fails() {
let (alice, tree) = new_tree("alice").await;
let res = CommitReceiver::new(
&tree,
alice,
alice,
test_cipher_suite_provider(TEST_CIPHER_SUITE),
)
.receive([Proposal::Add(Box::new(AddProposal {
key_package: key_package_with_invalid_signature().await,
}))])
.await;
assert_matches!(res, Err(MlsError::InvalidSignature));
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_additional_add_with_invalid_key_package_fails() {
let (alice, tree) = new_tree("alice").await;
let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.with_additional([Proposal::Add(Box::new(AddProposal {
key_package: key_package_with_invalid_signature().await,
}))])
.send()
.await;
assert_matches!(res, Err(MlsError::InvalidSignature));
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_add_with_invalid_key_package_filters_it_out() {
let (alice, tree) = new_tree("alice").await;
let proposal = Proposal::Add(Box::new(AddProposal {
key_package: key_package_with_invalid_signature().await,
}));
let proposal_info = make_proposal_info(&proposal, alice).await;
let processed_proposals =
CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.cache(
proposal_info.proposal_ref().unwrap().clone(),
proposal.clone(),
alice,
)
.send()
.await
.unwrap();
assert_eq!(processed_proposals.0, Vec::new());
#[cfg(feature = "state_update")]
assert_eq!(processed_proposals.1.unused_proposals, vec![proposal_info]);
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_add_with_hpke_key_of_another_member_fails() {
let (alice, tree) = new_tree("alice").await;
let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.with_additional([Proposal::Add(Box::new(AddProposal {
key_package: key_package_with_public_key(
tree.get_leaf_node(alice).unwrap().public_key.clone(),
)
.await,
}))])
.send()
.await;
assert_matches!(res, Err(MlsError::DuplicateLeafData(_)));
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_add_with_hpke_key_of_another_member_filters_it_out() {
let (alice, tree) = new_tree("alice").await;
let proposal = Proposal::Add(Box::new(AddProposal {
key_package: key_package_with_public_key(
tree.get_leaf_node(alice).unwrap().public_key.clone(),
)
.await,
}));
let proposal_info = make_proposal_info(&proposal, alice).await;
let processed_proposals =
CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.cache(
proposal_info.proposal_ref().unwrap().clone(),
proposal.clone(),
alice,
)
.send()
.await
.unwrap();
assert_eq!(processed_proposals.0, Vec::new());
#[cfg(feature = "state_update")]
assert_eq!(processed_proposals.1.unused_proposals, vec![proposal_info]);
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn receiving_update_with_invalid_leaf_node_fails() {
let (alice, mut tree) = new_tree("alice").await;
let bob = add_member(&mut tree, "bob").await;
let proposal = Proposal::Update(UpdateProposal {
leaf_node: get_basic_test_node(TEST_CIPHER_SUITE, "alice").await,
});
let proposal_ref = make_proposal_ref(&proposal, bob).await;
let res = CommitReceiver::new(
&tree,
alice,
bob,
test_cipher_suite_provider(TEST_CIPHER_SUITE),
)
.cache(proposal_ref.clone(), proposal, bob)
.receive([proposal_ref])
.await;
assert_matches!(res, Err(MlsError::InvalidLeafNodeSource));
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_update_with_invalid_leaf_node_filters_it_out() {
let (alice, mut tree) = new_tree("alice").await;
let bob = add_member(&mut tree, "bob").await;
let proposal = Proposal::Update(UpdateProposal {
leaf_node: get_basic_test_node(TEST_CIPHER_SUITE, "alice").await,
});
let proposal_info = make_proposal_info(&proposal, bob).await;
let processed_proposals =
CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.cache(proposal_info.proposal_ref().unwrap().clone(), proposal, bob)
.send()
.await
.unwrap();
assert_eq!(processed_proposals.0, Vec::new());
#[cfg(feature = "state_update")]
assert_eq!(processed_proposals.1.unused_proposals, vec![proposal_info]);
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn receiving_remove_with_invalid_index_fails() {
let (alice, tree) = new_tree("alice").await;
let res = CommitReceiver::new(
&tree,
alice,
alice,
test_cipher_suite_provider(TEST_CIPHER_SUITE),
)
.receive([Proposal::Remove(RemoveProposal {
to_remove: LeafIndex(10),
})])
.await;
assert_matches!(res, Err(MlsError::InvalidNodeIndex(20)));
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_additional_remove_with_invalid_index_fails() {
let (alice, tree) = new_tree("alice").await;
let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.with_additional([Proposal::Remove(RemoveProposal {
to_remove: LeafIndex(10),
})])
.send()
.await;
assert_matches!(res, Err(MlsError::InvalidNodeIndex(20)));
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_remove_with_invalid_index_filters_it_out() {
let (alice, tree) = new_tree("alice").await;
let proposal = Proposal::Remove(RemoveProposal {
to_remove: LeafIndex(10),
});
let proposal_info = make_proposal_info(&proposal, alice).await;
let processed_proposals =
CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.cache(
proposal_info.proposal_ref().unwrap().clone(),
proposal.clone(),
alice,
)
.send()
.await
.unwrap();
assert_eq!(processed_proposals.0, Vec::new());
#[cfg(feature = "state_update")]
assert_eq!(processed_proposals.1.unused_proposals, vec![proposal_info]);
}
#[cfg(feature = "psk")]
fn make_external_psk(id: &[u8], nonce: PskNonce) -> PreSharedKeyProposal {
PreSharedKeyProposal {
psk: PreSharedKeyID {
key_id: JustPreSharedKeyID::External(ExternalPskId::new(id.to_vec())),
psk_nonce: nonce,
},
}
}
#[cfg(feature = "psk")]
fn new_external_psk(id: &[u8]) -> PreSharedKeyProposal {
make_external_psk(
id,
PskNonce::random(&test_cipher_suite_provider(TEST_CIPHER_SUITE)).unwrap(),
)
}
#[cfg(feature = "psk")]
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn receiving_psk_with_invalid_nonce_fails() {
let invalid_nonce = PskNonce(vec![0, 1, 2]);
let (alice, tree) = new_tree("alice").await;
let res = CommitReceiver::new(
&tree,
alice,
alice,
test_cipher_suite_provider(TEST_CIPHER_SUITE),
)
.receive([Proposal::Psk(make_external_psk(
b"foo",
invalid_nonce.clone(),
))])
.await;
assert_matches!(res, Err(MlsError::InvalidPskNonceLength,));
}
#[cfg(feature = "psk")]
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_additional_psk_with_invalid_nonce_fails() {
let invalid_nonce = PskNonce(vec![0, 1, 2]);
let (alice, tree) = new_tree("alice").await;
let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.with_additional([Proposal::Psk(make_external_psk(
b"foo",
invalid_nonce.clone(),
))])
.send()
.await;
assert_matches!(res, Err(MlsError::InvalidPskNonceLength));
}
#[cfg(feature = "psk")]
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_psk_with_invalid_nonce_filters_it_out() {
let invalid_nonce = PskNonce(vec![0, 1, 2]);
let (alice, tree) = new_tree("alice").await;
let proposal = Proposal::Psk(make_external_psk(b"foo", invalid_nonce));
let proposal_info = make_proposal_info(&proposal, alice).await;
let processed_proposals =
CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.cache(
proposal_info.proposal_ref().unwrap().clone(),
proposal.clone(),
alice,
)
.send()
.await
.unwrap();
assert_eq!(processed_proposals.0, Vec::new());
#[cfg(feature = "state_update")]
assert_eq!(processed_proposals.1.unused_proposals, vec![proposal_info]);
}
#[cfg(feature = "psk")]
fn make_resumption_psk(usage: ResumptionPSKUsage) -> PreSharedKeyProposal {
PreSharedKeyProposal {
psk: PreSharedKeyID {
key_id: JustPreSharedKeyID::Resumption(ResumptionPsk {
usage,
psk_group_id: PskGroupId(TEST_GROUP.to_vec()),
psk_epoch: 1,
}),
psk_nonce: PskNonce::random(&test_cipher_suite_provider(TEST_CIPHER_SUITE))
.unwrap(),
},
}
}
#[cfg(feature = "psk")]
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
async fn receiving_resumption_psk_with_bad_usage_fails(usage: ResumptionPSKUsage) {
let (alice, tree) = new_tree("alice").await;
let res = CommitReceiver::new(
&tree,
alice,
alice,
test_cipher_suite_provider(TEST_CIPHER_SUITE),
)
.receive([Proposal::Psk(make_resumption_psk(usage))])
.await;
assert_matches!(res, Err(MlsError::InvalidTypeOrUsageInPreSharedKeyProposal));
}
#[cfg(feature = "psk")]
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
async fn sending_additional_resumption_psk_with_bad_usage_fails(usage: ResumptionPSKUsage) {
let (alice, tree) = new_tree("alice").await;
let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.with_additional([Proposal::Psk(make_resumption_psk(usage))])
.send()
.await;
assert_matches!(res, Err(MlsError::InvalidTypeOrUsageInPreSharedKeyProposal));
}
#[cfg(feature = "psk")]
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
async fn sending_resumption_psk_with_bad_usage_filters_it_out(usage: ResumptionPSKUsage) {
let (alice, tree) = new_tree("alice").await;
let proposal = Proposal::Psk(make_resumption_psk(usage));
let proposal_info = make_proposal_info(&proposal, alice).await;
let processed_proposals =
CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.cache(
proposal_info.proposal_ref().unwrap().clone(),
proposal.clone(),
alice,
)
.send()
.await
.unwrap();
assert_eq!(processed_proposals.0, Vec::new());
#[cfg(feature = "state_update")]
assert_eq!(processed_proposals.1.unused_proposals, vec![proposal_info]);
}
#[cfg(feature = "psk")]
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn receiving_resumption_psk_with_reinit_usage_fails() {
receiving_resumption_psk_with_bad_usage_fails(ResumptionPSKUsage::Reinit).await;
}
#[cfg(feature = "psk")]
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_additional_resumption_psk_with_reinit_usage_fails() {
sending_additional_resumption_psk_with_bad_usage_fails(ResumptionPSKUsage::Reinit).await;
}
#[cfg(feature = "psk")]
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_resumption_psk_with_reinit_usage_filters_it_out() {
sending_resumption_psk_with_bad_usage_filters_it_out(ResumptionPSKUsage::Reinit).await;
}
#[cfg(feature = "psk")]
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn receiving_resumption_psk_with_branch_usage_fails() {
receiving_resumption_psk_with_bad_usage_fails(ResumptionPSKUsage::Branch).await;
}
#[cfg(feature = "psk")]
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_additional_resumption_psk_with_branch_usage_fails() {
sending_additional_resumption_psk_with_bad_usage_fails(ResumptionPSKUsage::Branch).await;
}
#[cfg(feature = "psk")]
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_resumption_psk_with_branch_usage_filters_it_out() {
sending_resumption_psk_with_bad_usage_filters_it_out(ResumptionPSKUsage::Branch).await;
}
fn make_reinit(version: ProtocolVersion) -> ReInitProposal {
ReInitProposal {
group_id: TEST_GROUP.to_vec(),
version,
cipher_suite: TEST_CIPHER_SUITE,
extensions: ExtensionList::new(),
}
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn receiving_reinit_downgrading_version_fails() {
let smaller_protocol_version = ProtocolVersion::from(0);
let (alice, tree) = new_tree("alice").await;
let res = CommitReceiver::new(
&tree,
alice,
alice,
test_cipher_suite_provider(TEST_CIPHER_SUITE),
)
.receive([Proposal::ReInit(make_reinit(smaller_protocol_version))])
.await;
assert_matches!(res, Err(MlsError::InvalidProtocolVersionInReInit));
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_additional_reinit_downgrading_version_fails() {
let smaller_protocol_version = ProtocolVersion::from(0);
let (alice, tree) = new_tree("alice").await;
let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.with_additional([Proposal::ReInit(make_reinit(smaller_protocol_version))])
.send()
.await;
assert_matches!(res, Err(MlsError::InvalidProtocolVersionInReInit));
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_reinit_downgrading_version_filters_it_out() {
let smaller_protocol_version = ProtocolVersion::from(0);
let (alice, tree) = new_tree("alice").await;
let proposal = Proposal::ReInit(make_reinit(smaller_protocol_version));
let proposal_info = make_proposal_info(&proposal, alice).await;
let processed_proposals =
CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.cache(
proposal_info.proposal_ref().unwrap().clone(),
proposal.clone(),
alice,
)
.send()
.await
.unwrap();
assert_eq!(processed_proposals.0, Vec::new());
#[cfg(feature = "state_update")]
assert_eq!(processed_proposals.1.unused_proposals, vec![proposal_info]);
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn receiving_update_for_committer_fails() {
let (alice, tree) = new_tree("alice").await;
let update = Proposal::Update(make_update_proposal("alice").await);
let update_ref = make_proposal_ref(&update, alice).await;
let res = CommitReceiver::new(
&tree,
alice,
alice,
test_cipher_suite_provider(TEST_CIPHER_SUITE),
)
.cache(update_ref.clone(), update, alice)
.receive([update_ref])
.await;
assert_matches!(res, Err(MlsError::InvalidCommitSelfUpdate));
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_additional_update_for_committer_fails() {
let (alice, tree) = new_tree("alice").await;
let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.with_additional([Proposal::Update(make_update_proposal("alice").await)])
.send()
.await;
assert_matches!(res, Err(MlsError::InvalidProposalTypeForSender));
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_update_for_committer_filters_it_out() {
let (alice, tree) = new_tree("alice").await;
let proposal = Proposal::Update(make_update_proposal("alice").await);
let proposal_info = make_proposal_info(&proposal, alice).await;
let processed_proposals =
CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.cache(
proposal_info.proposal_ref().unwrap().clone(),
proposal.clone(),
alice,
)
.send()
.await
.unwrap();
assert_eq!(processed_proposals.0, Vec::new());
#[cfg(feature = "state_update")]
assert_eq!(processed_proposals.1.unused_proposals, vec![proposal_info]);
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn receiving_remove_for_committer_fails() {
let (alice, tree) = new_tree("alice").await;
let res = CommitReceiver::new(
&tree,
alice,
alice,
test_cipher_suite_provider(TEST_CIPHER_SUITE),
)
.receive([Proposal::Remove(RemoveProposal { to_remove: alice })])
.await;
assert_matches!(res, Err(MlsError::CommitterSelfRemoval));
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_additional_remove_for_committer_fails() {
let (alice, tree) = new_tree("alice").await;
let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.with_additional([Proposal::Remove(RemoveProposal { to_remove: alice })])
.send()
.await;
assert_matches!(res, Err(MlsError::CommitterSelfRemoval));
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_remove_for_committer_filters_it_out() {
let (alice, tree) = new_tree("alice").await;
let proposal = Proposal::Remove(RemoveProposal { to_remove: alice });
let proposal_info = make_proposal_info(&proposal, alice).await;
let processed_proposals =
CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.cache(
proposal_info.proposal_ref().unwrap().clone(),
proposal.clone(),
alice,
)
.send()
.await
.unwrap();
assert_eq!(processed_proposals.0, Vec::new());
#[cfg(feature = "state_update")]
assert_eq!(processed_proposals.1.unused_proposals, vec![proposal_info]);
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn receiving_update_and_remove_for_same_leaf_fails() {
let (alice, mut tree) = new_tree("alice").await;
let bob = add_member(&mut tree, "bob").await;
let update = Proposal::Update(make_update_proposal("bob").await);
let update_ref = make_proposal_ref(&update, bob).await;
let remove = Proposal::Remove(RemoveProposal { to_remove: bob });
let remove_ref = make_proposal_ref(&remove, bob).await;
let res = CommitReceiver::new(
&tree,
alice,
alice,
test_cipher_suite_provider(TEST_CIPHER_SUITE),
)
.cache(update_ref.clone(), update, bob)
.cache(remove_ref.clone(), remove, bob)
.receive([update_ref, remove_ref])
.await;
assert_matches!(res, Err(MlsError::UpdatingNonExistingMember));
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_update_and_remove_for_same_leaf_filters_update_out() {
let (alice, mut tree) = new_tree("alice").await;
let bob = add_member(&mut tree, "bob").await;
let update = Proposal::Update(make_update_proposal("bob").await);
let update_info = make_proposal_info(&update, alice).await;
let remove = Proposal::Remove(RemoveProposal { to_remove: bob });
let remove_ref = make_proposal_ref(&remove, alice).await;
let processed_proposals =
CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.cache(
update_info.proposal_ref().unwrap().clone(),
update.clone(),
alice,
)
.cache(remove_ref.clone(), remove, alice)
.send()
.await
.unwrap();
assert_eq!(processed_proposals.0, vec![remove_ref.into()]);
#[cfg(feature = "state_update")]
assert_eq!(processed_proposals.1.unused_proposals, vec![update_info]);
}
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
async fn make_add_proposal() -> Box<AddProposal> {
Box::new(AddProposal {
key_package: test_key_package(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "frank").await,
})
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn receiving_add_proposals_for_same_client_fails() {
let (alice, tree) = new_tree("alice").await;
let res = CommitReceiver::new(
&tree,
alice,
alice,
test_cipher_suite_provider(TEST_CIPHER_SUITE),
)
.receive([
Proposal::Add(make_add_proposal().await),
Proposal::Add(make_add_proposal().await),
])
.await;
assert_matches!(res, Err(MlsError::DuplicateLeafData(1)));
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_additional_add_proposals_for_same_client_fails() {
let (alice, tree) = new_tree("alice").await;
let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.with_additional([
Proposal::Add(make_add_proposal().await),
Proposal::Add(make_add_proposal().await),
])
.send()
.await;
assert_matches!(res, Err(MlsError::DuplicateLeafData(1)));
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_add_proposals_for_same_client_keeps_only_one() {
let (alice, tree) = new_tree("alice").await;
let add_one = Proposal::Add(make_add_proposal().await);
let add_two = Proposal::Add(make_add_proposal().await);
let add_ref_one = make_proposal_ref(&add_one, alice).await;
let add_ref_two = make_proposal_ref(&add_two, alice).await;
let processed_proposals =
CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.cache(add_ref_one.clone(), add_one.clone(), alice)
.cache(add_ref_two.clone(), add_two.clone(), alice)
.send()
.await
.unwrap();
let committed_add_ref = match &*processed_proposals.0 {
[ProposalOrRef::Reference(add_ref)] => add_ref,
_ => panic!("committed proposals list does not contain exactly one reference"),
};
let add_refs = [add_ref_one, add_ref_two];
assert!(add_refs.contains(committed_add_ref));
#[cfg(feature = "state_update")]
assert_matches!(
&*processed_proposals.1.unused_proposals,
[rejected_add_info] if committed_add_ref != rejected_add_info.proposal_ref().unwrap() && add_refs.contains(rejected_add_info.proposal_ref().unwrap())
);
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn receiving_update_for_different_identity_fails() {
let (alice, mut tree) = new_tree("alice").await;
let bob = add_member(&mut tree, "bob").await;
let update = Proposal::Update(make_update_proposal_custom("carol", 1).await);
let update_ref = make_proposal_ref(&update, bob).await;
let res = CommitReceiver::new(
&tree,
alice,
alice,
test_cipher_suite_provider(TEST_CIPHER_SUITE),
)
.cache(update_ref.clone(), update, bob)
.receive([update_ref])
.await;
assert_matches!(res, Err(MlsError::InvalidSuccessor));
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_update_for_different_identity_filters_it_out() {
let (alice, mut tree) = new_tree("alice").await;
let bob = add_member(&mut tree, "bob").await;
let update = Proposal::Update(make_update_proposal("carol").await);
let update_info = make_proposal_info(&update, bob).await;
let processed_proposals =
CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.cache(update_info.proposal_ref().unwrap().clone(), update, bob)
.send()
.await
.unwrap();
assert_eq!(processed_proposals.0, Vec::new());
// Bob proposed the update, so it is not listed as rejected when Alice commits it because
// she didn't propose it.
#[cfg(feature = "state_update")]
assert_eq!(processed_proposals.1.unused_proposals, vec![update_info]);
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn receiving_add_for_same_client_as_existing_member_fails() {
let (alice, public_tree) = new_tree("alice").await;
let add = Proposal::Add(make_add_proposal().await);
let ProvisionalState { public_tree, .. } = CommitReceiver::new(
&public_tree,
alice,
alice,
test_cipher_suite_provider(TEST_CIPHER_SUITE),
)
.receive([add.clone()])
.await
.unwrap();
let res = CommitReceiver::new(
&public_tree,
alice,
alice,
test_cipher_suite_provider(TEST_CIPHER_SUITE),
)
.receive([add])
.await;
assert_matches!(res, Err(MlsError::DuplicateLeafData(1)));
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_additional_add_for_same_client_as_existing_member_fails() {
let (alice, public_tree) = new_tree("alice").await;
let add = Proposal::Add(make_add_proposal().await);
let ProvisionalState { public_tree, .. } = CommitReceiver::new(
&public_tree,
alice,
alice,
test_cipher_suite_provider(TEST_CIPHER_SUITE),
)
.receive([add.clone()])
.await
.unwrap();
let res = CommitSender::new(
&public_tree,
alice,
test_cipher_suite_provider(TEST_CIPHER_SUITE),
)
.with_additional([add])
.send()
.await;
assert_matches!(res, Err(MlsError::DuplicateLeafData(1)));
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_add_for_same_client_as_existing_member_filters_it_out() {
let (alice, public_tree) = new_tree("alice").await;
let add = Proposal::Add(make_add_proposal().await);
let ProvisionalState { public_tree, .. } = CommitReceiver::new(
&public_tree,
alice,
alice,
test_cipher_suite_provider(TEST_CIPHER_SUITE),
)
.receive([add.clone()])
.await
.unwrap();
let proposal_info = make_proposal_info(&add, alice).await;
let processed_proposals = CommitSender::new(
&public_tree,
alice,
test_cipher_suite_provider(TEST_CIPHER_SUITE),
)
.cache(
proposal_info.proposal_ref().unwrap().clone(),
add.clone(),
alice,
)
.send()
.await
.unwrap();
assert_eq!(processed_proposals.0, Vec::new());
#[cfg(feature = "state_update")]
assert_eq!(processed_proposals.1.unused_proposals, vec![proposal_info]);
}
#[cfg(feature = "psk")]
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn receiving_psk_proposals_with_same_psk_id_fails() {
let (alice, tree) = new_tree("alice").await;
let psk_proposal = Proposal::Psk(new_external_psk(b"foo"));
let res = CommitReceiver::new(
&tree,
alice,
alice,
test_cipher_suite_provider(TEST_CIPHER_SUITE),
)
.receive([psk_proposal.clone(), psk_proposal])
.await;
assert_matches!(res, Err(MlsError::DuplicatePskIds));
}
#[cfg(feature = "psk")]
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_additional_psk_proposals_with_same_psk_id_fails() {
let (alice, tree) = new_tree("alice").await;
let psk_proposal = Proposal::Psk(new_external_psk(b"foo"));
let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.with_additional([psk_proposal.clone(), psk_proposal])
.send()
.await;
assert_matches!(res, Err(MlsError::DuplicatePskIds));
}
#[cfg(feature = "psk")]
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_psk_proposals_with_same_psk_id_keeps_only_one() {
let (alice, mut tree) = new_tree("alice").await;
let bob = add_member(&mut tree, "bob").await;
let proposal = Proposal::Psk(new_external_psk(b"foo"));
let proposal_info = [
make_proposal_info(&proposal, alice).await,
make_proposal_info(&proposal, bob).await,
];
let processed_proposals =
CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.cache(
proposal_info[0].proposal_ref().unwrap().clone(),
proposal.clone(),
alice,
)
.cache(
proposal_info[1].proposal_ref().unwrap().clone(),
proposal,
bob,
)
.send()
.await
.unwrap();
let committed_info = match processed_proposals
.1
.applied_proposals
.clone()
.into_proposals()
.collect_vec()
.as_slice()
{
[r] => r.clone(),
_ => panic!("Expected single proposal reference in {processed_proposals:?}"),
};
assert!(proposal_info.contains(&committed_info));
#[cfg(feature = "state_update")]
match &*processed_proposals.1.unused_proposals {
[r] => {
assert_ne!(*r, committed_info);
assert!(proposal_info.contains(r));
}
_ => panic!(
"Expected one proposal reference in {:?}",
processed_proposals.1.unused_proposals
),
}
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn receiving_multiple_group_context_extensions_fails() {
let (alice, tree) = new_tree("alice").await;
let res = CommitReceiver::new(
&tree,
alice,
alice,
test_cipher_suite_provider(TEST_CIPHER_SUITE),
)
.receive([
Proposal::GroupContextExtensions(ExtensionList::new()),
Proposal::GroupContextExtensions(ExtensionList::new()),
])
.await;
assert_matches!(
res,
Err(MlsError::MoreThanOneGroupContextExtensionsProposal)
);
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_multiple_additional_group_context_extensions_fails() {
let (alice, tree) = new_tree("alice").await;
let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.with_additional([
Proposal::GroupContextExtensions(ExtensionList::new()),
Proposal::GroupContextExtensions(ExtensionList::new()),
])
.send()
.await;
assert_matches!(
res,
Err(MlsError::MoreThanOneGroupContextExtensionsProposal)
);
}
fn make_extension_list(foo: u8) -> ExtensionList {
vec![TestExtension { foo }.into_extension().unwrap()].into()
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_multiple_group_context_extensions_keeps_only_one() {
let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
let (alice, tree) = {
let (signing_identity, signature_key) =
get_test_signing_identity(TEST_CIPHER_SUITE, b"alice").await;
let properties = ConfigProperties {
capabilities: Capabilities {
extensions: vec![42.into()],
..Capabilities::default()
},
extensions: Default::default(),
};
let (leaf, secret) = LeafNode::generate(
&cipher_suite_provider,
properties,
signing_identity,
&signature_key,
Lifetime::years(1).unwrap(),
)
.await
.unwrap();
let (pub_tree, priv_tree) =
TreeKemPublic::derive(leaf, secret, &BasicIdentityProvider, &Default::default())
.await
.unwrap();
(priv_tree.self_index, pub_tree)
};
let proposals = [
Proposal::GroupContextExtensions(make_extension_list(0)),
Proposal::GroupContextExtensions(make_extension_list(1)),
];
let gce_info = [
make_proposal_info(&proposals[0], alice).await,
make_proposal_info(&proposals[1], alice).await,
];
let processed_proposals =
CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.cache(
gce_info[0].proposal_ref().unwrap().clone(),
proposals[0].clone(),
alice,
)
.cache(
gce_info[1].proposal_ref().unwrap().clone(),
proposals[1].clone(),
alice,
)
.send()
.await
.unwrap();
let committed_gce_info = match processed_proposals
.1
.applied_proposals
.clone()
.into_proposals()
.collect_vec()
.as_slice()
{
[gce_info] => gce_info.clone(),
_ => panic!("committed proposals list does not contain exactly one reference"),
};
assert!(gce_info.contains(&committed_gce_info));
#[cfg(feature = "state_update")]
assert_matches!(
&*processed_proposals.1.unused_proposals,
[rejected_gce_info] if committed_gce_info != *rejected_gce_info && gce_info.contains(rejected_gce_info)
);
}
#[cfg(feature = "by_ref_proposal")]
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
async fn make_external_senders_extension() -> ExtensionList {
let identity = get_test_signing_identity(TEST_CIPHER_SUITE, b"alice")
.await
.0;
vec![ExternalSendersExt::new(vec![identity])
.into_extension()
.unwrap()]
.into()
}
#[cfg(feature = "by_ref_proposal")]
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn receiving_invalid_external_senders_extension_fails() {
let (alice, tree) = new_tree("alice").await;
let res = CommitReceiver::new(
&tree,
alice,
alice,
test_cipher_suite_provider(TEST_CIPHER_SUITE),
)
.with_identity_provider(FailureIdentityProvider::new())
.receive([Proposal::GroupContextExtensions(
make_external_senders_extension().await,
)])
.await;
assert_matches!(res, Err(MlsError::IdentityProviderError(_)));
}
#[cfg(feature = "by_ref_proposal")]
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_additional_invalid_external_senders_extension_fails() {
let (alice, tree) = new_tree("alice").await;
let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.with_identity_provider(FailureIdentityProvider::new())
.with_additional([Proposal::GroupContextExtensions(
make_external_senders_extension().await,
)])
.send()
.await;
assert_matches!(res, Err(MlsError::IdentityProviderError(_)));
}
#[cfg(feature = "by_ref_proposal")]
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_invalid_external_senders_extension_filters_it_out() {
let (alice, tree) = new_tree("alice").await;
let proposal = Proposal::GroupContextExtensions(make_external_senders_extension().await);
let proposal_info = make_proposal_info(&proposal, alice).await;
let processed_proposals =
CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.with_identity_provider(FailureIdentityProvider::new())
.cache(
proposal_info.proposal_ref().unwrap().clone(),
proposal.clone(),
alice,
)
.send()
.await
.unwrap();
assert_eq!(processed_proposals.0, Vec::new());
#[cfg(feature = "state_update")]
assert_eq!(processed_proposals.1.unused_proposals, vec![proposal_info]);
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn receiving_reinit_with_other_proposals_fails() {
let (alice, tree) = new_tree("alice").await;
let res = CommitReceiver::new(
&tree,
alice,
alice,
test_cipher_suite_provider(TEST_CIPHER_SUITE),
)
.receive([
Proposal::ReInit(make_reinit(TEST_PROTOCOL_VERSION)),
Proposal::Add(make_add_proposal().await),
])
.await;
assert_matches!(res, Err(MlsError::OtherProposalWithReInit));
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_additional_reinit_with_other_proposals_fails() {
let (alice, tree) = new_tree("alice").await;
let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.with_additional([
Proposal::ReInit(make_reinit(TEST_PROTOCOL_VERSION)),
Proposal::Add(make_add_proposal().await),
])
.send()
.await;
assert_matches!(res, Err(MlsError::OtherProposalWithReInit));
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_reinit_with_other_proposals_filters_it_out() {
let (alice, tree) = new_tree("alice").await;
let reinit = Proposal::ReInit(make_reinit(TEST_PROTOCOL_VERSION));
let reinit_info = make_proposal_info(&reinit, alice).await;
let add = Proposal::Add(make_add_proposal().await);
let add_ref = make_proposal_ref(&add, alice).await;
let processed_proposals =
CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.cache(
reinit_info.proposal_ref().unwrap().clone(),
reinit.clone(),
alice,
)
.cache(add_ref.clone(), add, alice)
.send()
.await
.unwrap();
assert_eq!(processed_proposals.0, vec![add_ref.into()]);
#[cfg(feature = "state_update")]
assert_eq!(processed_proposals.1.unused_proposals, vec![reinit_info]);
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn receiving_multiple_reinits_fails() {
let (alice, tree) = new_tree("alice").await;
let res = CommitReceiver::new(
&tree,
alice,
alice,
test_cipher_suite_provider(TEST_CIPHER_SUITE),
)
.receive([
Proposal::ReInit(make_reinit(TEST_PROTOCOL_VERSION)),
Proposal::ReInit(make_reinit(TEST_PROTOCOL_VERSION)),
])
.await;
assert_matches!(res, Err(MlsError::OtherProposalWithReInit));
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_additional_multiple_reinits_fails() {
let (alice, tree) = new_tree("alice").await;
let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.with_additional([
Proposal::ReInit(make_reinit(TEST_PROTOCOL_VERSION)),
Proposal::ReInit(make_reinit(TEST_PROTOCOL_VERSION)),
])
.send()
.await;
assert_matches!(res, Err(MlsError::OtherProposalWithReInit));
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_multiple_reinits_keeps_only_one() {
let (alice, tree) = new_tree("alice").await;
let reinit = Proposal::ReInit(make_reinit(TEST_PROTOCOL_VERSION));
let reinit_ref = make_proposal_ref(&reinit, alice).await;
let other_reinit = Proposal::ReInit(ReInitProposal {
group_id: b"other_group".to_vec(),
..make_reinit(TEST_PROTOCOL_VERSION)
});
let other_reinit_ref = make_proposal_ref(&other_reinit, alice).await;
let processed_proposals =
CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.cache(reinit_ref.clone(), reinit.clone(), alice)
.cache(other_reinit_ref.clone(), other_reinit.clone(), alice)
.send()
.await
.unwrap();
let processed_ref = match &*processed_proposals.0 {
[ProposalOrRef::Reference(r)] => r,
p => panic!("Expected single proposal reference but found {p:?}"),
};
assert!(*processed_ref == reinit_ref || *processed_ref == other_reinit_ref);
#[cfg(feature = "state_update")]
{
let (rejected_ref, unused_proposal) = match &*processed_proposals.1.unused_proposals {
[r] => (r.proposal_ref().unwrap().clone(), r.proposal.clone()),
p => panic!("Expected single proposal but found {p:?}"),
};
assert_ne!(rejected_ref, *processed_ref);
assert!(rejected_ref == reinit_ref || rejected_ref == other_reinit_ref);
assert!(unused_proposal == reinit || unused_proposal == other_reinit);
}
}
fn make_external_init() -> ExternalInit {
ExternalInit {
kem_output: vec![33; test_cipher_suite_provider(TEST_CIPHER_SUITE).kdf_extract_size()],
}
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn receiving_external_init_from_member_fails() {
let (alice, tree) = new_tree("alice").await;
let res = CommitReceiver::new(
&tree,
alice,
alice,
test_cipher_suite_provider(TEST_CIPHER_SUITE),
)
.receive([Proposal::ExternalInit(make_external_init())])
.await;
assert_matches!(res, Err(MlsError::InvalidProposalTypeForSender));
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_additional_external_init_from_member_fails() {
let (alice, tree) = new_tree("alice").await;
let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.with_additional([Proposal::ExternalInit(make_external_init())])
.send()
.await;
assert_matches!(res, Err(MlsError::InvalidProposalTypeForSender));
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_external_init_from_member_filters_it_out() {
let (alice, tree) = new_tree("alice").await;
let external_init = Proposal::ExternalInit(make_external_init());
let external_init_info = make_proposal_info(&external_init, alice).await;
let processed_proposals =
CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.cache(
external_init_info.proposal_ref().unwrap().clone(),
external_init.clone(),
alice,
)
.send()
.await
.unwrap();
assert_eq!(processed_proposals.0, Vec::new());
#[cfg(feature = "state_update")]
assert_eq!(
processed_proposals.1.unused_proposals,
vec![external_init_info]
);
}
fn required_capabilities_proposal(extension: u16) -> Proposal {
let required_capabilities = RequiredCapabilitiesExt {
extensions: vec![extension.into()],
..Default::default()
};
let ext = vec![required_capabilities.into_extension().unwrap()];
Proposal::GroupContextExtensions(ext.into())
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn receiving_required_capabilities_not_supported_by_member_fails() {
let (alice, tree) = new_tree("alice").await;
let res = CommitReceiver::new(
&tree,
alice,
alice,
test_cipher_suite_provider(TEST_CIPHER_SUITE),
)
.receive([required_capabilities_proposal(33)])
.await;
assert_matches!(
res,
Err(MlsError::RequiredExtensionNotFound(v)) if v == 33.into()
);
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_required_capabilities_not_supported_by_member_fails() {
let (alice, tree) = new_tree("alice").await;
let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.with_additional([required_capabilities_proposal(33)])
.send()
.await;
assert_matches!(
res,
Err(MlsError::RequiredExtensionNotFound(v)) if v == 33.into()
);
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_additional_required_capabilities_not_supported_by_member_filters_it_out() {
let (alice, tree) = new_tree("alice").await;
let proposal = required_capabilities_proposal(33);
let proposal_info = make_proposal_info(&proposal, alice).await;
let processed_proposals =
CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.cache(
proposal_info.proposal_ref().unwrap().clone(),
proposal.clone(),
alice,
)
.send()
.await
.unwrap();
assert_eq!(processed_proposals.0, Vec::new());
#[cfg(feature = "state_update")]
assert_eq!(processed_proposals.1.unused_proposals, vec![proposal_info]);
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn committing_update_from_pk1_to_pk2_and_update_from_pk2_to_pk3_works() {
let (alice_leaf, alice_secret, alice_signer) =
get_basic_test_node_sig_key(TEST_CIPHER_SUITE, "alice").await;
let (mut tree, priv_tree) = TreeKemPublic::derive(
alice_leaf.clone(),
alice_secret,
&BasicIdentityProvider,
&Default::default(),
)
.await
.unwrap();
let alice = priv_tree.self_index;
let bob = add_member(&mut tree, "bob").await;
let carol = add_member(&mut tree, "carol").await;
let bob_current_leaf = tree.get_leaf_node(bob).unwrap();
let mut alice_new_leaf = LeafNode {
public_key: bob_current_leaf.public_key.clone(),
leaf_node_source: LeafNodeSource::Update,
..alice_leaf
};
alice_new_leaf
.sign(
&test_cipher_suite_provider(TEST_CIPHER_SUITE),
&alice_signer,
&(TEST_GROUP, 0).into(),
)
.await
.unwrap();
let bob_new_leaf = update_leaf_node("bob", 1).await;
let pk1_to_pk2 = Proposal::Update(UpdateProposal {
leaf_node: alice_new_leaf.clone(),
});
let pk1_to_pk2_ref = make_proposal_ref(&pk1_to_pk2, alice).await;
let pk2_to_pk3 = Proposal::Update(UpdateProposal {
leaf_node: bob_new_leaf.clone(),
});
let pk2_to_pk3_ref = make_proposal_ref(&pk2_to_pk3, bob).await;
let effects = CommitReceiver::new(
&tree,
carol,
carol,
test_cipher_suite_provider(TEST_CIPHER_SUITE),
)
.cache(pk1_to_pk2_ref.clone(), pk1_to_pk2, alice)
.cache(pk2_to_pk3_ref.clone(), pk2_to_pk3, bob)
.receive([pk1_to_pk2_ref, pk2_to_pk3_ref])
.await
.unwrap();
assert_eq!(effects.applied_proposals.update_senders, vec![alice, bob]);
assert_eq!(
effects
.applied_proposals
.updates
.into_iter()
.map(|p| p.proposal.leaf_node)
.collect_vec(),
vec![alice_new_leaf, bob_new_leaf]
);
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn committing_update_from_pk1_to_pk2_and_removal_of_pk2_works() {
let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE);
let (alice_leaf, alice_secret, alice_signer) =
get_basic_test_node_sig_key(TEST_CIPHER_SUITE, "alice").await;
let (mut tree, priv_tree) = TreeKemPublic::derive(
alice_leaf.clone(),
alice_secret,
&BasicIdentityProvider,
&Default::default(),
)
.await
.unwrap();
let alice = priv_tree.self_index;
let bob = add_member(&mut tree, "bob").await;
let carol = add_member(&mut tree, "carol").await;
let bob_current_leaf = tree.get_leaf_node(bob).unwrap();
let mut alice_new_leaf = LeafNode {
public_key: bob_current_leaf.public_key.clone(),
leaf_node_source: LeafNodeSource::Update,
..alice_leaf
};
alice_new_leaf
.sign(
&cipher_suite_provider,
&alice_signer,
&(TEST_GROUP, 0).into(),
)
.await
.unwrap();
let pk1_to_pk2 = Proposal::Update(UpdateProposal {
leaf_node: alice_new_leaf.clone(),
});
let pk1_to_pk2_ref = make_proposal_ref(&pk1_to_pk2, alice).await;
let remove_pk2 = Proposal::Remove(RemoveProposal { to_remove: bob });
let remove_pk2_ref = make_proposal_ref(&remove_pk2, bob).await;
let effects = CommitReceiver::new(
&tree,
carol,
carol,
test_cipher_suite_provider(TEST_CIPHER_SUITE),
)
.cache(pk1_to_pk2_ref.clone(), pk1_to_pk2, alice)
.cache(remove_pk2_ref.clone(), remove_pk2, bob)
.receive([pk1_to_pk2_ref, remove_pk2_ref])
.await
.unwrap();
assert_eq!(effects.applied_proposals.update_senders, vec![alice]);
assert_eq!(
effects
.applied_proposals
.updates
.into_iter()
.map(|p| p.proposal.leaf_node)
.collect_vec(),
vec![alice_new_leaf]
);
assert_eq!(
effects
.applied_proposals
.removals
.into_iter()
.map(|p| p.proposal.to_remove)
.collect_vec(),
vec![bob]
);
}
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
async fn unsupported_credential_key_package(name: &str) -> KeyPackage {
let (mut signing_identity, secret_key) =
get_test_signing_identity(TEST_CIPHER_SUITE, name.as_bytes()).await;
signing_identity.credential = Credential::Custom(CustomCredential::new(
CredentialType::new(BasicWithCustomProvider::CUSTOM_CREDENTIAL_TYPE),
random_bytes(32),
));
let generator = KeyPackageGenerator {
protocol_version: TEST_PROTOCOL_VERSION,
cipher_suite_provider: &test_cipher_suite_provider(TEST_CIPHER_SUITE),
signing_identity: &signing_identity,
signing_key: &secret_key,
identity_provider: &BasicWithCustomProvider::new(BasicIdentityProvider::new()),
};
generator
.generate(
Lifetime::years(1).unwrap(),
Capabilities {
credentials: vec![42.into()],
..Default::default()
},
Default::default(),
Default::default(),
)
.await
.unwrap()
.key_package
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn receiving_add_with_leaf_not_supporting_credential_type_of_other_leaf_fails() {
let (alice, tree) = new_tree("alice").await;
let res = CommitReceiver::new(
&tree,
alice,
alice,
test_cipher_suite_provider(TEST_CIPHER_SUITE),
)
.receive([Proposal::Add(Box::new(AddProposal {
key_package: unsupported_credential_key_package("bob").await,
}))])
.await;
assert_matches!(res, Err(MlsError::InUseCredentialTypeUnsupportedByNewLeaf));
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_additional_add_with_leaf_not_supporting_credential_type_of_other_leaf_fails() {
let (alice, tree) = new_tree("alice").await;
let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.with_additional([Proposal::Add(Box::new(AddProposal {
key_package: unsupported_credential_key_package("bob").await,
}))])
.send()
.await;
assert_matches!(res, Err(MlsError::InUseCredentialTypeUnsupportedByNewLeaf));
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_add_with_leaf_not_supporting_credential_type_of_other_leaf_filters_it_out() {
let (alice, tree) = new_tree("alice").await;
let add = Proposal::Add(Box::new(AddProposal {
key_package: unsupported_credential_key_package("bob").await,
}));
let add_info = make_proposal_info(&add, alice).await;
let processed_proposals =
CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.cache(add_info.proposal_ref().unwrap().clone(), add.clone(), alice)
.send()
.await
.unwrap();
assert_eq!(processed_proposals.0, Vec::new());
#[cfg(feature = "state_update")]
assert_eq!(processed_proposals.1.unused_proposals, vec![add_info]);
}
#[cfg(feature = "custom_proposal")]
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_custom_proposal_with_member_not_supporting_proposal_type_fails() {
let (alice, tree) = new_tree("alice").await;
let custom_proposal = Proposal::Custom(CustomProposal::new(ProposalType::new(42), vec![]));
let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.with_additional([custom_proposal.clone()])
.send()
.await;
assert_matches!(
res,
Err(
MlsError::UnsupportedCustomProposal(c)
) if c == custom_proposal.proposal_type()
);
}
#[cfg(feature = "custom_proposal")]
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_custom_proposal_with_member_not_supporting_filters_it_out() {
let (alice, tree) = new_tree("alice").await;
let custom_proposal = Proposal::Custom(CustomProposal::new(ProposalType::new(42), vec![]));
let custom_info = make_proposal_info(&custom_proposal, alice).await;
let processed_proposals =
CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.cache(
custom_info.proposal_ref().unwrap().clone(),
custom_proposal.clone(),
alice,
)
.send()
.await
.unwrap();
assert_eq!(processed_proposals.0, Vec::new());
#[cfg(feature = "state_update")]
assert_eq!(processed_proposals.1.unused_proposals, vec![custom_info]);
}
#[cfg(feature = "custom_proposal")]
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn receiving_custom_proposal_with_member_not_supporting_fails() {
let (alice, tree) = new_tree("alice").await;
let custom_proposal = Proposal::Custom(CustomProposal::new(ProposalType::new(42), vec![]));
let res = CommitReceiver::new(
&tree,
alice,
alice,
test_cipher_suite_provider(TEST_CIPHER_SUITE),
)
.receive([custom_proposal.clone()])
.await;
assert_matches!(
res,
Err(MlsError::UnsupportedCustomProposal(c)) if c == custom_proposal.proposal_type()
);
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn receiving_group_extension_unsupported_by_leaf_fails() {
let (alice, tree) = new_tree("alice").await;
let res = CommitReceiver::new(
&tree,
alice,
alice,
test_cipher_suite_provider(TEST_CIPHER_SUITE),
)
.receive([Proposal::GroupContextExtensions(make_extension_list(0))])
.await;
assert_matches!(
res,
Err(
MlsError::UnsupportedGroupExtension(v)
) if v == 42.into()
);
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_additional_group_extension_unsupported_by_leaf_fails() {
let (alice, tree) = new_tree("alice").await;
let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.with_additional([Proposal::GroupContextExtensions(make_extension_list(0))])
.send()
.await;
assert_matches!(
res,
Err(
MlsError::UnsupportedGroupExtension(v)
) if v == 42.into()
);
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_group_extension_unsupported_by_leaf_filters_it_out() {
let (alice, tree) = new_tree("alice").await;
let proposal = Proposal::GroupContextExtensions(make_extension_list(0));
let proposal_info = make_proposal_info(&proposal, alice).await;
let processed_proposals =
CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.cache(
proposal_info.proposal_ref().unwrap().clone(),
proposal.clone(),
alice,
)
.send()
.await
.unwrap();
assert_eq!(processed_proposals.0, Vec::new());
#[cfg(feature = "state_update")]
assert_eq!(processed_proposals.1.unused_proposals, vec![proposal_info]);
}
#[cfg(feature = "psk")]
#[derive(Debug)]
struct AlwaysNotFoundPskStorage;
#[cfg(feature = "psk")]
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
#[cfg_attr(mls_build_async, maybe_async::must_be_async)]
impl PreSharedKeyStorage for AlwaysNotFoundPskStorage {
type Error = Infallible;
async fn get(&self, _: &ExternalPskId) -> Result<Option<PreSharedKey>, Self::Error> {
Ok(None)
}
}
#[cfg(feature = "psk")]
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn receiving_external_psk_with_unknown_id_fails() {
let (alice, tree) = new_tree("alice").await;
let res = CommitReceiver::new(
&tree,
alice,
alice,
test_cipher_suite_provider(TEST_CIPHER_SUITE),
)
.with_psk_storage(AlwaysNotFoundPskStorage)
.receive([Proposal::Psk(new_external_psk(b"abc"))])
.await;
assert_matches!(res, Err(MlsError::MissingRequiredPsk));
}
#[cfg(feature = "psk")]
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_additional_external_psk_with_unknown_id_fails() {
let (alice, tree) = new_tree("alice").await;
let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.with_psk_storage(AlwaysNotFoundPskStorage)
.with_additional([Proposal::Psk(new_external_psk(b"abc"))])
.send()
.await;
assert_matches!(res, Err(MlsError::MissingRequiredPsk));
}
#[cfg(feature = "psk")]
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn sending_external_psk_with_unknown_id_filters_it_out() {
let (alice, tree) = new_tree("alice").await;
let proposal = Proposal::Psk(new_external_psk(b"abc"));
let proposal_info = make_proposal_info(&proposal, alice).await;
let processed_proposals =
CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.with_psk_storage(AlwaysNotFoundPskStorage)
.cache(
proposal_info.proposal_ref().unwrap().clone(),
proposal.clone(),
alice,
)
.send()
.await
.unwrap();
assert_eq!(processed_proposals.0, Vec::new());
#[cfg(feature = "state_update")]
assert_eq!(processed_proposals.1.unused_proposals, vec![proposal_info]);
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn user_defined_filter_can_remove_proposals() {
struct RemoveGroupContextExtensions;
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
#[cfg_attr(mls_build_async, maybe_async::must_be_async)]
impl MlsRules for RemoveGroupContextExtensions {
type Error = Infallible;
async fn filter_proposals(
&self,
_: CommitDirection,
_: CommitSource,
_: &Roster,
_: &ExtensionList,
mut proposals: ProposalBundle,
) -> Result<ProposalBundle, Self::Error> {
proposals.group_context_extensions.clear();
Ok(proposals)
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn commit_options(
&self,
_: &Roster,
_: &ExtensionList,
_: &ProposalBundle,
) -> Result<CommitOptions, Self::Error> {
Ok(Default::default())
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn encryption_options(
&self,
_: &Roster,
_: &ExtensionList,
) -> Result<EncryptionOptions, Self::Error> {
Ok(Default::default())
}
}
let (alice, tree) = new_tree("alice").await;
let (committed, _) =
CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.with_additional([Proposal::GroupContextExtensions(Default::default())])
.with_user_rules(RemoveGroupContextExtensions)
.send()
.await
.unwrap();
assert_eq!(committed, Vec::new());
}
struct FailureMlsRules;
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
#[cfg_attr(mls_build_async, maybe_async::must_be_async)]
impl MlsRules for FailureMlsRules {
type Error = MlsError;
async fn filter_proposals(
&self,
_: CommitDirection,
_: CommitSource,
_: &Roster,
_: &ExtensionList,
_: ProposalBundle,
) -> Result<ProposalBundle, Self::Error> {
Err(MlsError::InvalidSignature)
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn commit_options(
&self,
_: &Roster,
_: &ExtensionList,
_: &ProposalBundle,
) -> Result<CommitOptions, Self::Error> {
Ok(Default::default())
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn encryption_options(
&self,
_: &Roster,
_: &ExtensionList,
) -> Result<EncryptionOptions, Self::Error> {
Ok(Default::default())
}
}
struct InjectMlsRules {
to_inject: Proposal,
source: ProposalSource,
}
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
#[cfg_attr(mls_build_async, maybe_async::must_be_async)]
impl MlsRules for InjectMlsRules {
type Error = MlsError;
async fn filter_proposals(
&self,
_: CommitDirection,
_: CommitSource,
_: &Roster,
_: &ExtensionList,
mut proposals: ProposalBundle,
) -> Result<ProposalBundle, Self::Error> {
proposals.add(
self.to_inject.clone(),
Sender::Member(0),
self.source.clone(),
);
Ok(proposals)
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn commit_options(
&self,
_: &Roster,
_: &ExtensionList,
_: &ProposalBundle,
) -> Result<CommitOptions, Self::Error> {
Ok(Default::default())
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn encryption_options(
&self,
_: &Roster,
_: &ExtensionList,
) -> Result<EncryptionOptions, Self::Error> {
Ok(Default::default())
}
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn user_defined_filter_can_inject_proposals() {
let (alice, tree) = new_tree("alice").await;
let test_proposal = Proposal::GroupContextExtensions(Default::default());
let (committed, _) =
CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.with_user_rules(InjectMlsRules {
to_inject: test_proposal.clone(),
source: ProposalSource::ByValue,
})
.send()
.await
.unwrap();
assert_eq!(
committed,
vec![ProposalOrRef::Proposal(test_proposal.into())]
);
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn user_defined_filter_can_inject_local_only_proposals() {
let (alice, tree) = new_tree("alice").await;
let test_proposal = Proposal::GroupContextExtensions(Default::default());
let (committed, _) =
CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.with_user_rules(InjectMlsRules {
to_inject: test_proposal.clone(),
source: ProposalSource::Local,
})
.send()
.await
.unwrap();
assert_eq!(committed, vec![]);
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn user_defined_filter_cant_break_base_rules() {
let (alice, tree) = new_tree("alice").await;
let test_proposal = Proposal::Update(UpdateProposal {
leaf_node: get_basic_test_node(TEST_CIPHER_SUITE, "leaf").await,
});
let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.with_user_rules(InjectMlsRules {
to_inject: test_proposal.clone(),
source: ProposalSource::ByValue,
})
.send()
.await;
assert_matches!(res, Err(MlsError::InvalidProposalTypeForSender { .. }))
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn user_defined_filter_can_refuse_to_send_commit() {
let (alice, tree) = new_tree("alice").await;
let res = CommitSender::new(&tree, alice, test_cipher_suite_provider(TEST_CIPHER_SUITE))
.with_additional([Proposal::GroupContextExtensions(Default::default())])
.with_user_rules(FailureMlsRules)
.send()
.await;
assert_matches!(res, Err(MlsError::MlsRulesError(_)));
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn user_defined_filter_can_reject_incoming_commit() {
let (alice, tree) = new_tree("alice").await;
let res = CommitReceiver::new(
&tree,
alice,
alice,
test_cipher_suite_provider(TEST_CIPHER_SUITE),
)
.with_user_rules(FailureMlsRules)
.receive([Proposal::GroupContextExtensions(Default::default())])
.await;
assert_matches!(res, Err(MlsError::MlsRulesError(_)));
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn proposers_are_verified() {
let (alice, mut tree) = new_tree("alice").await;
let bob = add_member(&mut tree, "bob").await;
#[cfg(feature = "by_ref_proposal")]
let identity = get_test_signing_identity(TEST_CIPHER_SUITE, b"carol")
.await
.0;
#[cfg(feature = "by_ref_proposal")]
let external_senders = ExternalSendersExt::new(vec![identity]);
let proposals: &[Proposal] = &[
Proposal::Add(make_add_proposal().await),
Proposal::Update(make_update_proposal("alice").await),
Proposal::Remove(RemoveProposal { to_remove: bob }),
#[cfg(feature = "psk")]
Proposal::Psk(make_external_psk(
b"ted",
PskNonce::random(&test_cipher_suite_provider(TEST_CIPHER_SUITE)).unwrap(),
)),
Proposal::ReInit(make_reinit(TEST_PROTOCOL_VERSION)),
Proposal::ExternalInit(make_external_init()),
Proposal::GroupContextExtensions(Default::default()),
];
let proposers = [
Sender::Member(*alice),
#[cfg(feature = "by_ref_proposal")]
Sender::External(0),
Sender::NewMemberCommit,
Sender::NewMemberProposal,
];
for ((proposer, proposal), by_ref) in proposers
.into_iter()
.cartesian_product(proposals)
.cartesian_product([true])
{
let committer = Sender::Member(*alice);
let receiver = CommitReceiver::new(
&tree,
committer,
alice,
test_cipher_suite_provider(TEST_CIPHER_SUITE),
);
#[cfg(feature = "by_ref_proposal")]
let extensions: ExtensionList =
vec![external_senders.clone().into_extension().unwrap()].into();
#[cfg(feature = "by_ref_proposal")]
let receiver = receiver.with_extensions(extensions);
let (receiver, proposals, proposer) = if by_ref {
let proposal_ref = make_proposal_ref(proposal, proposer).await;
let receiver = receiver.cache(proposal_ref.clone(), proposal.clone(), proposer);
(receiver, vec![ProposalOrRef::from(proposal_ref)], proposer)
} else {
(receiver, vec![proposal.clone().into()], committer)
};
let res = receiver.receive(proposals).await;
if proposer_can_propose(proposer, proposal.proposal_type(), by_ref).is_err() {
assert_matches!(res, Err(MlsError::InvalidProposalTypeForSender));
} else {
let is_self_update = proposal.proposal_type() == ProposalType::UPDATE
&& by_ref
&& matches!(proposer, Sender::Member(_));
if !is_self_update {
res.unwrap();
}
}
}
}
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
async fn make_update_proposal(name: &str) -> UpdateProposal {
UpdateProposal {
leaf_node: update_leaf_node(name, 1).await,
}
}
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
async fn make_update_proposal_custom(name: &str, leaf_index: u32) -> UpdateProposal {
UpdateProposal {
leaf_node: update_leaf_node(name, leaf_index).await,
}
}
#[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
async fn when_receiving_commit_unused_proposals_are_proposals_in_cache_but_not_in_commit() {
let (alice, tree) = new_tree("alice").await;
let proposal = Proposal::GroupContextExtensions(Default::default());
let proposal_ref = make_proposal_ref(&proposal, alice).await;
let state = CommitReceiver::new(
&tree,
alice,
alice,
test_cipher_suite_provider(TEST_CIPHER_SUITE),
)
.cache(proposal_ref.clone(), proposal, alice)
.receive([Proposal::Add(Box::new(AddProposal {
key_package: test_key_package(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "bob").await,
}))])
.await
.unwrap();
let [p] = &state.unused_proposals[..] else {
panic!(
"Expected single unused proposal but got {:?}",
state.unused_proposals
);
};
assert_eq!(p.proposal_ref(), Some(&proposal_ref));
}
}