Quelle proposal_cache.rs
Sprache: unbekannt
|
|
// 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,
}
}
--> --------------------
--> maximum size reached
--> --------------------
[ Dauer der Verarbeitung: 0.18 Sekunden
(vorverarbeitet)
]
|
2026-04-04
|