use mls_group::tests_and_kats::utils::setup_client;
use openmls_basic_credential::SignatureKeyPair;
use openmls_test::openmls_test;
use openmls_traits::types::Ciphersuite;
use tls_codec::{Deserialize as _, Serialize as _};
use crate::{
ciphersuite::hash_ref::ProposalRef,
credentials::CredentialWithKey,
framing::*,
group::*,
key_packages::{errors::KeyPackageVerifyError, *},
messages::group_info::GroupInfo,
test_utils::frankenstein::{self, FrankenMlsMessage},
treesync::{
errors::LeafNodeValidationError, node::leaf_node::Capabilities, LeafNodeParameters,
},
};
struct MemberState<Provider> {
party: PartyState<Provider>,
group: MlsGroup,
}
#[allow(dead_code)]
struct PartyState<Provider> {
provider: Provider,
credential_with_key: CredentialWithKey,
key_package_bundle: KeyPackageBundle,
signer: SignatureKeyPair,
sig_pk: OpenMlsSignaturePublicKey,
name: &'static str,
}
impl<Provider: crate::storage::OpenMlsProvider + Default> PartyState<Provider> {
fn generate(name: &'static str, ciphersuite: Ciphersuite) -> Self {
let provider = Provider::default();
let (credential_with_key, key_package_bundle, signer, sig_pk) =
setup_client(name, ciphersuite, &provider);
PartyState {
provider,
name,
credential_with_key,
key_package_bundle,
signer,
sig_pk,
}
}
fn key_package<F: FnOnce(KeyPackageBuilder) -> KeyPackageBuilder>(
&self,
ciphersuite: Ciphersuite,
f: F,
) -> KeyPackageBundle {
f(KeyPackage::builder())
.build(
ciphersuite,
&self.provider,
&self.signer,
self.credential_with_key.clone(),
)
.unwrap_or_else(|err| panic!("failed to build key package at {}: {err}", self.name))
}
}
struct TestState<Provider> {
alice: MemberState<Provider>,
bob: MemberState<Provider>,
}
fn setup<Provider: crate::storage::OpenMlsProvider + Default>(
ciphersuite: Ciphersuite,
) -> TestState<Provider> {
let alice_party = PartyState::generate("alice", ciphersuite);
let bob_party = PartyState::generate("bob", ciphersuite);
let alice_group = MlsGroup::builder()
.ciphersuite(ciphersuite)
.with_wire_format_policy(WireFormatPolicy::new(
OutgoingWireFormatPolicy::AlwaysPlaintext,
IncomingWireFormatPolicy::Mixed,
))
.with_capabilities(
Capabilities::builder()
.extensions(vec![
ExtensionType::Unknown(0xf001),
ExtensionType::Unknown(0xf002),
])
.build(),
)
.build(
&alice_party.provider,
&alice_party.signer,
alice_party.credential_with_key.clone(),
)
.expect("error creating group using builder");
let mut alice = MemberState {
party: alice_party,
group: alice_group,
};
let bob_key_package = bob_party.key_package(ciphersuite, |builder| {
builder.leaf_node_capabilities(
Capabilities::builder()
.extensions(vec![
ExtensionType::Unknown(0xf001),
ExtensionType::Unknown(0xf002),
ExtensionType::Unknown(0xf003),
])
.build(),
)
});
alice.propose_add_member(bob_key_package.key_package());
let (_, Some(welcome), _) = alice.commit_and_merge_pending() else {
panic!("expected receiving a welcome")
};
let welcome: MlsMessageIn = welcome.into();
let welcome = welcome
.into_welcome()
.expect("expected message to be a welcome");
let bob_group = StagedWelcome::new_from_welcome(
&bob_party.provider,
alice.group.configuration(),
welcome,
Some(alice.group.export_ratchet_tree().into()),
)
.expect("Error creating staged join from Welcome")
.into_group(&bob_party.provider)
.expect("Error creating group from staged join");
TestState {
alice,
bob: MemberState {
party: bob_party,
group: bob_group,
},
}
}
impl<Provider: crate::storage::OpenMlsProvider> MemberState<Provider> {
fn propose_group_context_extensions(
&mut self,
extensions: Extensions<GroupContext>,
) -> (MlsMessageOut, ProposalRef) {
self.group
.propose_group_context_extensions(&self.party.provider, extensions, &self.party.signer)
.unwrap_or_else(|err| panic!("couldn't propose GCE at {}: {err}", self.party.name))
}
fn update_group_context_extensions(
&mut self,
extensions: Extensions<GroupContext>,
) -> (MlsMessageOut, Option<MlsMessageOut>, Option<GroupInfo>) {
self.group
.update_group_context_extensions(&self.party.provider, extensions, &self.party.signer)
.unwrap_or_else(|err| panic!("couldn't propose GCE at {}: {err}", self.party.name))
}
fn propose_add_member(&mut self, key_package: &KeyPackage) -> (MlsMessageOut, ProposalRef) {
self.group
.propose_add_member(&self.party.provider, &self.party.signer, key_package)
.unwrap_or_else(|err| panic!("failed to propose member at {}: {err}", self.party.name))
}
fn process_and_merge_commit(&mut self, msg: MlsMessageIn) {
let msg = msg.into_protocol_message().unwrap();
let processed_msg = self
.group
.process_message(&self.party.provider, msg)
.unwrap_or_else(|err| panic!("error processing message at {}: {err}", self.party.name));
match processed_msg.into_content() {
ProcessedMessageContent::StagedCommitMessage(staged_commit) => self
.group
.merge_staged_commit(&self.party.provider, *staged_commit)
.unwrap_or_else(|err| {
panic!("error merging staged commit at {}: {err}", self.party.name)
}),
other => {
panic!(
"expected a commit message at {}, got {:?}",
self.party.name, other
)
}
}
}
fn process_and_store_proposal(&mut self, msg: MlsMessageIn) -> ProposalRef {
let msg = msg.into_protocol_message().unwrap();
let processed_msg = self
.group
.process_message(&self.party.provider, msg)
.unwrap_or_else(|err| panic!("error processing message at {}: {err}", self.party.name));
match processed_msg.into_content() {
ProcessedMessageContent::ProposalMessage(proposal) => {
let reference = proposal.proposal_reference();
self.group
.store_pending_proposal(self.party.provider.storage(), *proposal)
.unwrap_or_else(|err| {
panic!("error storing proposal at {}: {err}", self.party.name)
});
reference
}
other => {
panic!(
"expected a proposal message at {}, got {:?}",
self.party.name, other
)
}
}
}
fn fail_processing(
&mut self,
msg: MlsMessageIn,
) -> ProcessMessageError<Provider::StorageError> {
let msg = msg.into_protocol_message().unwrap();
let err_msg = format!(
"expected an error when processing message at {}",
self.party.name
);
self.group
.process_message(&self.party.provider, msg)
.expect_err(&err_msg)
}
fn commit_to_pending_proposals(
&mut self,
) -> (MlsMessageOut, Option<MlsMessageOut>, Option<GroupInfo>) {
self.group
.commit_to_pending_proposals(&self.party.provider, &self.party.signer)
.unwrap_or_else(|err| {
panic!(
"{} couldn't commit pending proposal: {err}",
self.party.name
)
})
}
fn merge_pending_commit(&mut self) {
self.group
.merge_pending_commit(&self.party.provider)
.unwrap_or_else(|err| panic!("{} couldn't merge commit: {err}", self.party.name));
}
fn commit_and_merge_pending(
&mut self,
) -> (MlsMessageOut, Option<MlsMessageOut>, Option<GroupInfo>) {
let commit_out = self.commit_to_pending_proposals();
self.merge_pending_commit();
commit_out
}
}
#[openmls_test]
fn happy_case() {
let TestState { mut alice, mut bob } = setup::<Provider>(ciphersuite);
let (commit, _, _) = alice.update_group_context_extensions(
Extensions::single(Extension::RequiredCapabilities(
RequiredCapabilitiesExtension::new(&[ExtensionType::Unknown(0xf001)], &[], &[]),
))
.expect("failed to create single-element extensions list"),
);
alice.merge_pending_commit();
bob.process_and_merge_commit(commit.into());
let (proposal, _) = bob.propose_group_context_extensions(
Extensions::single(Extension::RequiredCapabilities(
RequiredCapabilitiesExtension::new(
&[
ExtensionType::Unknown(0xf001),
ExtensionType::Unknown(0xf002),
],
&[],
&[],
),
))
.expect("failed to create single-element extensions list"),
);
alice.process_and_store_proposal(proposal.into());
let (commit, _, _) = alice.commit_and_merge_pending();
bob.process_and_merge_commit(commit.into());
}
#[openmls_test]
fn self_update_happy_case() {
let TestState { mut alice, mut bob } = setup::<Provider>(ciphersuite);
let (update_prop, _) = bob
.group
.propose_self_update(
&bob.party.provider,
&bob.party.signer,
LeafNodeParameters::builder().build(),
)
.unwrap();
alice.process_and_store_proposal(update_prop.into());
let (commit, _, _) = alice.commit_and_merge_pending();
bob.process_and_merge_commit(commit.into())
}
#[openmls_test]
fn self_update_happy_case_simple() {
let alice_party = PartyState::<Provider>::generate("alice", ciphersuite);
let bob_party = PartyState::<Provider>::generate("bob", ciphersuite);
let mut alice_group = MlsGroup::builder()
.ciphersuite(ciphersuite)
.with_wire_format_policy(WireFormatPolicy::new(
OutgoingWireFormatPolicy::AlwaysPlaintext,
IncomingWireFormatPolicy::Mixed,
))
.build(
&alice_party.provider,
&alice_party.signer,
alice_party.credential_with_key.clone(),
)
.expect("error creating group using builder");
let bob_key_package = bob_party.key_package(ciphersuite, |builder| builder);
alice_group
.propose_add_member(
&alice_party.provider,
&alice_party.signer,
bob_key_package.key_package(),
)
.unwrap();
let (_, Some(welcome), _) = alice_group
.commit_to_pending_proposals(&alice_party.provider, &alice_party.signer)
.unwrap()
else {
panic!("expected receiving a welcome")
};
alice_group
.merge_pending_commit(&alice_party.provider)
.unwrap();
let welcome: MlsMessageIn = welcome.into();
let welcome = welcome
.into_welcome()
.expect("expected message to be a welcome");
let mut bob_group = StagedWelcome::new_from_welcome(
&bob_party.provider,
alice_group.configuration(),
welcome,
Some(alice_group.export_ratchet_tree().into()),
)
.expect("Error creating staged join from Welcome")
.into_group(&bob_party.provider)
.expect("Error creating group from staged join");
let (update_proposal_msg, _) = bob_group
.propose_self_update(
&bob_party.provider,
&bob_party.signer,
LeafNodeParameters::builder().build(),
)
.unwrap();
let ProcessedMessageContent::ProposalMessage(update_proposal) = alice_group
.process_message(
&alice_party.provider,
update_proposal_msg.clone().into_protocol_message().unwrap(),
)
.unwrap()
.into_content()
else {
panic!("expected a proposal, got {update_proposal_msg:?}");
};
alice_group
.store_pending_proposal(alice_party.provider.storage(), *update_proposal)
.unwrap();
let (commit_msg, _, _) = alice_group
.commit_to_pending_proposals(&alice_party.provider, &alice_party.signer)
.unwrap();
bob_group
.process_message(
&bob_party.provider,
commit_msg.into_protocol_message().unwrap(),
)
.unwrap();
bob_group.merge_pending_commit(&bob_party.provider).unwrap()
}
#[openmls_test]
fn fail_insufficient_extensiontype_capabilities_add_valn0103() {
let TestState { mut alice, mut bob } = setup::<Provider>(ciphersuite);
let (gce_req_cap_commit, _, _) = alice.update_group_context_extensions(
Extensions::single(Extension::RequiredCapabilities(
RequiredCapabilitiesExtension::new(&[ExtensionType::Unknown(0xf002)], &[], &[]),
))
.expect("failed to create single-element extensions list"),
);
alice.merge_pending_commit();
bob.process_and_merge_commit(gce_req_cap_commit.clone().into());
let frankenstein::FrankenMlsMessage {
version,
body:
frankenstein::FrankenMlsMessageBody::PublicMessage(frankenstein::FrankenPublicMessage {
content:
frankenstein::FrankenFramedContent {
group_id,
epoch: gce_commit_epoch,
sender,
authenticated_data,
..
},
..
}),
} = frankenstein::FrankenMlsMessage::from(gce_req_cap_commit)
else {
unreachable!()
};
let charlie = PartyState::<Provider>::generate("charlie", ciphersuite);
let charlie_kpb = charlie.key_package(ciphersuite, |builder| {
builder.leaf_node_capabilities(
Capabilities::builder()
.extensions(vec![ExtensionType::Unknown(0xf001)])
.build(),
)
});
let commit_content = frankenstein::FrankenFramedContent {
body: frankenstein::FrankenFramedContentBody::Commit(frankenstein::FrankenCommit {
proposals: vec![frankenstein::FrankenProposalOrRef::Proposal(
frankenstein::FrankenProposal::Add(frankenstein::FrankenAddProposal {
key_package: charlie_kpb.key_package.into(),
}),
)],
path: None,
}),
group_id,
epoch: gce_commit_epoch + 1,
sender,
authenticated_data,
};
let group_context = alice.group.export_group_context().clone();
let bob_group_context = bob.group.export_group_context();
assert_eq!(
bob_group_context.confirmed_transcript_hash(),
group_context.confirmed_transcript_hash()
);
let secrets = alice.group.message_secrets();
let membership_key = secrets.membership_key().as_slice();
let franken_commit = frankenstein::FrankenMlsMessage {
version,
body: frankenstein::FrankenMlsMessageBody::PublicMessage(
frankenstein::FrankenPublicMessage::auth(
&alice.party.provider,
ciphersuite,
&alice.party.signer,
commit_content,
Some(&group_context.into()),
Some(membership_key),
Some(vec![0u8; 32].into()),
),
),
};
let fake_commit = MlsMessageIn::tls_deserialize(
&mut franken_commit.tls_serialize_detached().unwrap().as_slice(),
)
.unwrap();
let err = bob.fail_processing(fake_commit);
assert!(
matches!(
err,
ProcessMessageError::InvalidCommit(StageCommitError::ProposalValidationError(
ProposalValidationError::LeafNodeValidation(
LeafNodeValidationError::UnsupportedExtensions
)
))
),
"got wrong error: {err:#?}"
);
}
#[openmls_test]
fn fail_insufficient_extensiontype_capabilities_update_valn0103() {
let TestState { mut alice, mut bob } = setup::<Provider>(ciphersuite);
let (gce_req_cap_commit, _, _) = alice.update_group_context_extensions(
Extensions::single(Extension::RequiredCapabilities(
RequiredCapabilitiesExtension::new(&[ExtensionType::Unknown(0xf002)], &[], &[]),
))
.expect("failed to create single-element extensions list"),
);
alice.merge_pending_commit();
bob.process_and_merge_commit(gce_req_cap_commit.clone().into());
let (update_prop, _) = bob
.group
.propose_self_update(
&bob.party.provider,
&bob.party.signer,
LeafNodeParameters::builder().build(),
)
.unwrap();
bob.group
.clear_pending_proposals(bob.party.provider.storage())
.unwrap();
let frankenstein::FrankenMlsMessage {
version,
body:
frankenstein::FrankenMlsMessageBody::PublicMessage(frankenstein::FrankenPublicMessage {
content: mut franken_proposal_content,
..
}),
} = frankenstein::FrankenMlsMessage::from(update_prop.clone())
else {
unreachable!()
};
let frankenstein::FrankenFramedContent {
body:
frankenstein::FrankenFramedContentBody::Proposal(frankenstein::FrankenProposal::Update(
frankenstein::FrankenUpdateProposal {
leaf_node: bob_franken_leaf_node,
},
)),
..
} = &mut franken_proposal_content
else {
unreachable!();
};
assert_eq!(
bob_franken_leaf_node.capabilities.extensions.remove(1),
0xf002
);
bob_franken_leaf_node.resign(
Some(frankenstein::FrankenTreePosition {
group_id: bob.group.group_id().as_slice().to_vec().into(),
leaf_index: bob.group.own_leaf_index().u32(),
}),
&bob.party.signer,
);
let group_context = bob.group.export_group_context().clone();
let secrets = bob.group.message_secrets();
let membership_key = secrets.membership_key().as_slice();
let franken_proposal = frankenstein::FrankenMlsMessage {
version,
body: frankenstein::FrankenMlsMessageBody::PublicMessage(
frankenstein::FrankenPublicMessage::auth(
&bob.party.provider,
ciphersuite,
&bob.party.signer,
franken_proposal_content.clone(),
Some(&group_context.into()),
Some(membership_key),
None,
),
),
};
let fake_proposal = MlsMessageIn::tls_deserialize(
&mut franken_proposal
.tls_serialize_detached()
.unwrap()
.as_slice(),
)
.unwrap();
alice.process_and_store_proposal(fake_proposal.clone());
let proposal_ref = bob.process_and_store_proposal(fake_proposal);
let alice_sender = frankenstein::FrankenSender::Member(0);
let commit_content = frankenstein::FrankenFramedContent {
sender: alice_sender,
body: frankenstein::FrankenFramedContentBody::Commit(frankenstein::FrankenCommit {
proposals: vec![frankenstein::FrankenProposalOrRef::Reference(
proposal_ref.as_slice().to_vec().into(),
)],
path: None,
}),
..franken_proposal_content
};
let group_context = alice.group.export_group_context().clone();
let secrets = alice.group.message_secrets();
let membership_key = secrets.membership_key().as_slice();
let franken_commit = frankenstein::FrankenMlsMessage {
version,
body: frankenstein::FrankenMlsMessageBody::PublicMessage(
frankenstein::FrankenPublicMessage::auth(
&alice.party.provider,
ciphersuite,
&alice.party.signer,
commit_content,
Some(&group_context.into()),
Some(membership_key),
Some(vec![0; 32].into()),
),
),
};
let fake_commit = MlsMessageIn::tls_deserialize(
&mut franken_commit.tls_serialize_detached().unwrap().as_slice(),
)
.unwrap();
let err = bob.fail_processing(fake_commit);
assert!(
matches!(
err,
ProcessMessageError::InvalidCommit(StageCommitError::ProposalValidationError(
ProposalValidationError::InsufficientCapabilities
))
),
"expected a different error, got: {err} ({err:#?})"
);
}
#[openmls_test]
fn fail_key_package_version_valn0201() {
let TestState { mut alice, mut bob } = setup::<Provider>(ciphersuite);
let charlie = PartyState::<Provider>::generate("charlie", ciphersuite);
let charlie_key_package_bundle = charlie.key_package(ciphersuite, |b| b);
let charlie_key_package = charlie_key_package_bundle.key_package();
let (original_proposal, _) = alice.propose_add_member(charlie_key_package);
alice
.group
.clear_pending_proposals(alice.party.provider.storage())
.unwrap();
let Ok(frankenstein::FrankenMlsMessage {
version,
body:
frankenstein::FrankenMlsMessageBody::PublicMessage(frankenstein::FrankenPublicMessage {
content:
frankenstein::FrankenFramedContent {
group_id,
epoch,
sender,
authenticated_data,
body:
frankenstein::FrankenFramedContentBody::Proposal(
frankenstein::FrankenProposal::Add(
frankenstein::FrankenAddProposal { mut key_package },
),
),
},
..
}),
}) = frankenstein::FrankenMlsMessage::tls_deserialize(
&mut original_proposal
.tls_serialize_detached()
.unwrap()
.as_slice(),
)
else {
panic!("proposal message has unexpected format: {original_proposal:#?}")
};
key_package.protocol_version = 2;
key_package.resign(&charlie.signer);
let group_context = alice.group.export_group_context();
let membership_key = alice.group.message_secrets().membership_key();
let franken_commit_message = frankenstein::FrankenMlsMessage {
version,
body: frankenstein::FrankenMlsMessageBody::PublicMessage(
frankenstein::FrankenPublicMessage::auth(
&alice.party.provider,
ciphersuite,
&alice.party.signer,
frankenstein::FrankenFramedContent {
group_id,
epoch,
sender,
authenticated_data,
body: frankenstein::FrankenFramedContentBody::Commit(
frankenstein::FrankenCommit {
proposals: vec![frankenstein::FrankenProposalOrRef::Proposal(
frankenstein::FrankenProposal::Add(
frankenstein::FrankenAddProposal { key_package },
),
)],
path: None,
},
),
},
Some(&group_context.clone().into()),
Some(membership_key.as_slice()),
Some(vec![0; 32].into()),
),
),
};
let fake_commit_message = MlsMessageIn::tls_deserialize(
&mut franken_commit_message
.tls_serialize_detached()
.unwrap()
.as_slice(),
)
.unwrap();
let err = {
let validation_skip_handle = crate::skip_validation::checks::confirmation_tag::handle();
validation_skip_handle.with_disabled(|| bob.fail_processing(fake_commit_message.clone()))
};
assert!(matches!(
err,
ProcessMessageError::ValidationError(ValidationError::KeyPackageVerifyError(
KeyPackageVerifyError::InvalidProtocolVersion
))
));
}
#[openmls_test]
fn fail_2_gce_proposals_1_commit_valn0308() {
let TestState { mut alice, mut bob } = setup::<Provider>(ciphersuite);
assert!(alice
.group
.context()
.extensions()
.required_capabilities()
.is_none());
let new_extensions = Extensions::single(Extension::RequiredCapabilities(
RequiredCapabilitiesExtension::new(&[ExtensionType::Unknown(0xf001)], &[], &[]),
))
.expect("failed to create single-element extensions list");
let (proposal, _) = alice.propose_group_context_extensions(new_extensions.clone());
bob.process_and_store_proposal(proposal.into());
assert_eq!(alice.group.pending_proposals().count(), 1);
let (commit, _, _) = alice.commit_to_pending_proposals();
let mut franken_commit = FrankenMlsMessage::tls_deserialize(
&mut commit.tls_serialize_detached().unwrap().as_slice(),
)
.unwrap();
match &mut franken_commit.body {
frankenstein::FrankenMlsMessageBody::PublicMessage(msg) => {
match &mut msg.content.body {
frankenstein::FrankenFramedContentBody::Commit(commit) => {
let second_gces = frankenstein::FrankenProposalOrRef::Proposal(
frankenstein::FrankenProposal::GroupContextExtensions(vec![
frankenstein::FrankenExtension::RequiredCapabilities(
frankenstein::FrankenRequiredCapabilitiesExtension {
extension_types: vec![],
proposal_types: vec![],
credential_types: vec![],
},
),
]),
);
commit.proposals.push(second_gces);
}
_ => unreachable!(),
}
let group_context = alice.group.export_group_context().clone();
let bob_group_context = bob.group.export_group_context();
assert_eq!(
bob_group_context.confirmed_transcript_hash(),
group_context.confirmed_transcript_hash()
);
let secrets = alice.group.message_secrets();
let membership_key = secrets.membership_key().as_slice();
*msg = frankenstein::FrankenPublicMessage::auth(
&alice.party.provider,
group_context.ciphersuite(),
&alice.party.signer,
msg.content.clone(),
Some(&group_context.into()),
Some(membership_key),
Some(vec![0u8; 32].into()),
);
}
_ => unreachable!(),
}
let fake_commit = MlsMessageIn::tls_deserialize(
&mut franken_commit.tls_serialize_detached().unwrap().as_slice(),
)
.unwrap();
let err = {
let validation_skip_handle = crate::skip_validation::checks::confirmation_tag::handle();
validation_skip_handle.with_disabled(|| bob.fail_processing(fake_commit.clone()))
};
assert!(matches!(
err,
ProcessMessageError::InvalidCommit(
StageCommitError::GroupContextExtensionsProposalValidationError(
GroupContextExtensionsProposalValidationError::TooManyGCEProposals
)
)
));
}
#[openmls_test]
fn fail_unsupported_gces_add_valn1001() {
let TestState { mut alice, mut bob }: TestState<Provider> = setup(ciphersuite);
assert!(alice
.group
.context()
.extensions()
.required_capabilities()
.is_none());
let new_extensions = Extensions::single(Extension::RequiredCapabilities(
RequiredCapabilitiesExtension::new(&[ExtensionType::Unknown(0xf001)], &[], &[]),
))
.expect("failed to create single-element extensions list");
let (original_proposal, _) = bob.propose_group_context_extensions(new_extensions.clone());
assert_eq!(bob.group.pending_proposals().count(), 1);
bob.group
.clear_pending_proposals(bob.party.provider.storage())
.unwrap();
let Ok(frankenstein::FrankenMlsMessage {
version,
body:
frankenstein::FrankenMlsMessageBody::PublicMessage(frankenstein::FrankenPublicMessage {
content:
frankenstein::FrankenFramedContent {
group_id,
epoch,
sender: bob_sender,
authenticated_data,
body:
frankenstein::FrankenFramedContentBody::Proposal(
frankenstein::FrankenProposal::GroupContextExtensions(mut gces),
),
},
..
}),
}) = frankenstein::FrankenMlsMessage::tls_deserialize(
&mut original_proposal
.tls_serialize_detached()
.unwrap()
.as_slice(),
)
else {
panic!("proposal message has unexpected format: {original_proposal:#?}")
};
let Some(frankenstein::FrankenExtension::RequiredCapabilities(
frankenstein::FrankenRequiredCapabilitiesExtension {
extension_types, ..
},
)) = gces.get_mut(0)
else {
panic!("required capabilities are malformed")
};
extension_types.push(0xf003);
let group_context = bob.group.export_group_context().clone();
let secrets = bob.group.message_secrets();
let membership_key = secrets.membership_key().as_slice();
let franken_commit_message = frankenstein::FrankenMlsMessage {
version,
body: frankenstein::FrankenMlsMessageBody::PublicMessage(
frankenstein::FrankenPublicMessage::auth(
&bob.party.provider,
ciphersuite,
&bob.party.signer,
frankenstein::FrankenFramedContent {
group_id,
epoch,
sender: bob_sender,
authenticated_data,
body: frankenstein::FrankenFramedContentBody::Commit(
frankenstein::FrankenCommit {
proposals: vec![frankenstein::FrankenProposalOrRef::Proposal(
frankenstein::FrankenProposal::GroupContextExtensions(gces),
)],
path: None,
},
),
},
Some(&group_context.into()),
Some(membership_key),
Some(vec![0u8; 32].into()),
),
),
};
let fake_commit = MlsMessageIn::tls_deserialize(
&mut franken_commit_message
.tls_serialize_detached()
.unwrap()
.as_slice(),
)
.unwrap();
let err = {
let validation_skip_handle = crate::skip_validation::checks::confirmation_tag::handle();
validation_skip_handle.with_disabled(|| alice.fail_processing(fake_commit.clone()))
};
assert!(
matches!(
err,
ProcessMessageError::InvalidCommit(
StageCommitError::GroupContextExtensionsProposalValidationError(
GroupContextExtensionsProposalValidationError::RequiredExtensionNotSupportedByAllMembers
)
)
),
"expected different error. got {err:?}"
);
}
#[openmls_test]
fn proposal() {
let TestState { mut alice, mut bob }: TestState<Provider> = setup(ciphersuite);
assert!(alice
.group
.context()
.extensions()
.required_capabilities()
.is_none());
let new_extensions = Extensions::single(Extension::RequiredCapabilities(
RequiredCapabilitiesExtension::new(&[ExtensionType::Unknown(0xf001)], &[], &[]),
))
.expect("failed to create single-element extensions list");
let (proposal, _) = alice.propose_group_context_extensions(new_extensions.clone());
bob.process_and_store_proposal(proposal.into());
assert_eq!(alice.group.pending_proposals().count(), 1);
let (commit, _, _) = alice.commit_and_merge_pending();
bob.process_and_merge_commit(commit.into());
assert_eq!(alice.group.pending_proposals().count(), 0);
let required_capabilities = alice
.group
.context()
.extensions()
.required_capabilities()
.expect("couldn't get required_capabilities");
assert!(required_capabilities.extension_types() == [ExtensionType::Unknown(0xf001)]);
let new_extensions_2 = Extensions::single(Extension::RequiredCapabilities(
RequiredCapabilitiesExtension::new(&[ExtensionType::RatchetTree], &[], &[]),
))
.expect("failed to create single-element extensions list");
alice
.group
.propose_group_context_extensions(
&alice.party.provider,
new_extensions,
&alice.party.signer,
)
.expect("failed to build group context extensions proposal");
alice
.group
.propose_group_context_extensions(
&alice.party.provider,
new_extensions_2,
&alice.party.signer,
)
.expect("failed to build group context extensions proposal");
assert_eq!(alice.group.pending_proposals().count(), 2);
alice
.group
.commit_to_pending_proposals(&alice.party.provider, &alice.party.signer)
.expect_err(
"expected error when committing to multiple group context extensions proposals",
);
let new_extensions = Extensions::single(Extension::RequiredCapabilities(
RequiredCapabilitiesExtension::new(&[ExtensionType::Unknown(0xf042)], &[], &[]),
))
.expect("failed to create single-element extensions list");
alice
.group
.propose_group_context_extensions(
&alice.party.provider,
new_extensions,
&alice.party.signer,
)
.expect_err("expected an error building GCE proposal with bad required_capabilities");
}
#[openmls_test]
fn fail_insufficient_extensiontype_capabilities_update_proposal_valn0502() {
let alice_party = PartyState::<Provider>::generate("alice", ciphersuite);
let bob_party = PartyState::<Provider>::generate("bob", ciphersuite);
let gc_extensions = Extensions::single(Extension::Unknown(
0xf003,
crate::extensions::UnknownExtension(vec![0x01]),
))
.expect("unknown extensions should be considered valid in group context");
let alice_group = MlsGroup::builder()
.ciphersuite(ciphersuite)
.with_wire_format_policy(WireFormatPolicy::new(
OutgoingWireFormatPolicy::AlwaysPlaintext,
IncomingWireFormatPolicy::Mixed,
))
.with_capabilities(
Capabilities::builder()
.extensions(vec![
ExtensionType::Unknown(0xf001),
ExtensionType::Unknown(0xf002),
ExtensionType::Unknown(0xf003),
])
.build(),
)
.with_group_context_extensions(gc_extensions)
.build(
&alice_party.provider,
&alice_party.signer,
alice_party.credential_with_key.clone(),
)
.expect("error creating group using builder");
let mut alice = MemberState {
party: alice_party,
group: alice_group,
};
let bob_key_package = bob_party.key_package(ciphersuite, |builder| {
builder.leaf_node_capabilities(
Capabilities::builder()
.extensions(vec![
ExtensionType::Unknown(0xf001),
ExtensionType::Unknown(0xf002),
ExtensionType::Unknown(0xf003),
])
.build(),
)
});
alice.propose_add_member(bob_key_package.key_package());
let (_, Some(welcome), _) = alice.commit_and_merge_pending() else {
panic!("expected receiving a welcome")
};
let welcome: MlsMessageIn = welcome.into();
let welcome = welcome
.into_welcome()
.expect("expected message to be a welcome");
let bob_group = StagedWelcome::new_from_welcome(
&bob_party.provider,
alice.group.configuration(),
welcome,
Some(alice.group.export_ratchet_tree().into()),
)
.expect("Error creating staged join from Welcome")
.into_group(&bob_party.provider)
.expect("Error creating group from staged join");
let mut bob = MemberState {
party: bob_party,
group: bob_group,
};
let (update_prop, _) = bob
.group
.propose_self_update(
&bob.party.provider,
&bob.party.signer,
LeafNodeParameters::builder().build(),
)
.unwrap();
bob.group
.clear_pending_proposals(bob.party.provider.storage())
.unwrap();
let frankenstein::FrankenMlsMessage {
version,
body:
frankenstein::FrankenMlsMessageBody::PublicMessage(frankenstein::FrankenPublicMessage {
content: mut franken_proposal_content,
..
}),
} = frankenstein::FrankenMlsMessage::from(update_prop.clone())
else {
unreachable!()
};
let frankenstein::FrankenFramedContent {
body:
frankenstein::FrankenFramedContentBody::Proposal(frankenstein::FrankenProposal::Update(
frankenstein::FrankenUpdateProposal {
leaf_node: bob_franken_leaf_node,
},
)),
..
} = &mut franken_proposal_content
else {
unreachable!();
};
bob_franken_leaf_node
.capabilities
.extensions
.retain(|&e| e != 0xf003);
bob_franken_leaf_node.resign(
Some(frankenstein::FrankenTreePosition {
group_id: bob.group.group_id().as_slice().to_vec().into(),
leaf_index: bob.group.own_leaf_index().u32(),
}),
&bob.party.signer,
);
let group_context = bob.group.export_group_context().clone();
let secrets = bob.group.message_secrets();
let membership_key = secrets.membership_key().as_slice();
let franken_proposal = frankenstein::FrankenMlsMessage {
version,
body: frankenstein::FrankenMlsMessageBody::PublicMessage(
frankenstein::FrankenPublicMessage::auth(
&bob.party.provider,
ciphersuite,
&bob.party.signer,
franken_proposal_content.clone(),
Some(&group_context.into()),
Some(membership_key),
None, ),
),
};
let fake_proposal = MlsMessageIn::tls_deserialize(
&mut franken_proposal
.tls_serialize_detached()
.unwrap()
.as_slice(),
)
.unwrap();
alice.process_and_store_proposal(fake_proposal.clone());
let proposal_ref = bob.process_and_store_proposal(fake_proposal);
let alice_sender = frankenstein::FrankenSender::Member(0);
let commit_content = frankenstein::FrankenFramedContent {
sender: alice_sender,
body: frankenstein::FrankenFramedContentBody::Commit(frankenstein::FrankenCommit {
proposals: vec![frankenstein::FrankenProposalOrRef::Reference(
proposal_ref.as_slice().to_vec().into(),
)],
path: None,
}),
..franken_proposal_content
};
let group_context = alice.group.export_group_context().clone();
let secrets = alice.group.message_secrets();
let membership_key = secrets.membership_key().as_slice();
let franken_commit = frankenstein::FrankenMlsMessage {
version,
body: frankenstein::FrankenMlsMessageBody::PublicMessage(
frankenstein::FrankenPublicMessage::auth(
&alice.party.provider,
ciphersuite,
&alice.party.signer,
commit_content,
Some(&group_context.into()),
Some(membership_key),
Some(vec![0; 32].into()), ),
),
};
let fake_commit = MlsMessageIn::tls_deserialize(
&mut franken_commit.tls_serialize_detached().unwrap().as_slice(),
)
.unwrap();
let err = bob.fail_processing(fake_commit);
assert!(
matches!(
err,
ProcessMessageError::InvalidCommit(StageCommitError::ProposalValidationError(
ProposalValidationError::LeafNodeValidation(
LeafNodeValidationError::UnsupportedExtensions
)
))
),
"expected UnsupportedExtensions error, got: {err} ({err:#?})"
);
}
#[openmls_test]
fn fail_insufficient_extensiontype_capabilities_commit_path_valn0502() {
let alice_party = PartyState::<Provider>::generate("alice", ciphersuite);
let bob_party = PartyState::<Provider>::generate("bob", ciphersuite);
let gc_extensions = Extensions::single(Extension::Unknown(
0xf003,
crate::extensions::UnknownExtension(vec![0x01]),
))
.expect("unknown extensions should be considered valid in group context");
let alice_group = MlsGroup::builder()
.ciphersuite(ciphersuite)
.with_wire_format_policy(WireFormatPolicy::new(
OutgoingWireFormatPolicy::AlwaysPlaintext,
IncomingWireFormatPolicy::Mixed,
))
.with_capabilities(
Capabilities::builder()
.extensions(vec![
ExtensionType::Unknown(0xf001),
ExtensionType::Unknown(0xf002),
ExtensionType::Unknown(0xf003),
])
.build(),
)
.with_group_context_extensions(gc_extensions)
.build(
&alice_party.provider,
&alice_party.signer,
alice_party.credential_with_key.clone(),
)
.expect("error creating group using builder");
let mut alice = MemberState {
party: alice_party,
group: alice_group,
};
let bob_key_package = bob_party.key_package(ciphersuite, |builder| {
builder.leaf_node_capabilities(
Capabilities::builder()
.extensions(vec![
ExtensionType::Unknown(0xf001),
ExtensionType::Unknown(0xf002),
ExtensionType::Unknown(0xf003),
])
.build(),
)
});
alice.propose_add_member(bob_key_package.key_package());
let (_, Some(welcome), _) = alice.commit_and_merge_pending() else {
panic!("expected receiving a welcome")
};
let welcome: MlsMessageIn = welcome.into();
let welcome = welcome
.into_welcome()
.expect("expected message to be a welcome");
let bob_group = StagedWelcome::new_from_welcome(
&bob_party.provider,
alice.group.configuration(),
welcome,
Some(alice.group.export_ratchet_tree().into()),
)
.expect("Error creating staged join from Welcome")
.into_group(&bob_party.provider)
.expect("Error creating group from staged join");
let mut bob = MemberState {
party: bob_party,
group: bob_group,
};
let commit_bundle = bob
.group
.commit_builder()
.force_self_update(true)
.load_psks(bob.party.provider.storage())
.unwrap()
.build(
bob.party.provider.rand(),
bob.party.provider.crypto(),
&bob.party.signer,
|_| true,
)
.unwrap()
.stage_commit(&bob.party.provider)
.unwrap();
let commit_msg = commit_bundle.commit().clone();
bob.group
.clear_pending_commit(bob.party.provider.storage())
.unwrap();
let frankenstein::FrankenMlsMessage {
version,
body:
frankenstein::FrankenMlsMessageBody::PublicMessage(frankenstein::FrankenPublicMessage {
content: mut franken_commit_content,
..
}),
} = frankenstein::FrankenMlsMessage::from(commit_msg.clone())
else {
unreachable!()
};
let frankenstein::FrankenFramedContent {
body:
frankenstein::FrankenFramedContentBody::Commit(frankenstein::FrankenCommit {
path: Some(ref mut path),
..
}),
..
} = &mut franken_commit_content
else {
unreachable!("expected commit with update path");
};
path.leaf_node
.capabilities
.extensions
.retain(|&e| e != 0xf003);
path.leaf_node.resign(
Some(frankenstein::FrankenTreePosition {
group_id: bob.group.group_id().as_slice().to_vec().into(),
leaf_index: bob.group.own_leaf_index().u32(),
}),
&bob.party.signer,
);
let group_context = bob.group.export_group_context().clone();
let secrets = bob.group.message_secrets();
let membership_key = secrets.membership_key().as_slice();
let franken_commit = frankenstein::FrankenMlsMessage {
version,
body: frankenstein::FrankenMlsMessageBody::PublicMessage(
frankenstein::FrankenPublicMessage::auth(
&bob.party.provider,
ciphersuite,
&bob.party.signer,
franken_commit_content,
Some(&group_context.into()),
Some(membership_key),
Some(vec![0; 32].into()), ),
),
};
let fake_commit = MlsMessageIn::tls_deserialize(
&mut franken_commit.tls_serialize_detached().unwrap().as_slice(),
)
.unwrap();
let err = alice.fail_processing(fake_commit);
assert!(
matches!(
err,
ProcessMessageError::InvalidCommit(StageCommitError::LeafNodeValidation(
LeafNodeValidationError::UnsupportedExtensions
))
),
"expected UnsupportedExtensions error, got: {err} ({err:#?})"
);
}
#[openmls_test]
fn fail_create_update_proposal_insufficient_capabilities() {
let alice_party = PartyState::<Provider>::generate("alice", ciphersuite);
let bob_party = PartyState::<Provider>::generate("bob", ciphersuite);
let gc_extensions = Extensions::single(Extension::Unknown(
0xf003,
crate::extensions::UnknownExtension(vec![0x01]),
))
.expect("unknown extensions should be considered valid in group context");
let alice_group = MlsGroup::builder()
.ciphersuite(ciphersuite)
.with_wire_format_policy(WireFormatPolicy::new(
OutgoingWireFormatPolicy::AlwaysPlaintext,
IncomingWireFormatPolicy::Mixed,
))
.with_capabilities(
Capabilities::builder()
.extensions(vec![
ExtensionType::Unknown(0xf001),
ExtensionType::Unknown(0xf002),
ExtensionType::Unknown(0xf003),
])
.build(),
)
.with_group_context_extensions(gc_extensions)
.build(
&alice_party.provider,
&alice_party.signer,
alice_party.credential_with_key.clone(),
)
.expect("error creating group using builder");
let mut alice = MemberState {
party: alice_party,
group: alice_group,
};
let bob_key_package = bob_party.key_package(ciphersuite, |builder| {
builder.leaf_node_capabilities(
Capabilities::builder()
.extensions(vec![
ExtensionType::Unknown(0xf001),
ExtensionType::Unknown(0xf002),
ExtensionType::Unknown(0xf003),
])
.build(),
)
});
alice.propose_add_member(bob_key_package.key_package());
let (_, Some(welcome), _) = alice.commit_and_merge_pending() else {
panic!("expected receiving a welcome")
};
let welcome: MlsMessageIn = welcome.into();
let welcome = welcome
.into_welcome()
.expect("expected message to be a welcome");
let bob_group = StagedWelcome::new_from_welcome(
&bob_party.provider,
alice.group.configuration(),
welcome,
Some(alice.group.export_ratchet_tree().into()),
)
.expect("Error creating staged join from Welcome")
.into_group(&bob_party.provider)
.expect("Error creating group from staged join");
let mut bob = MemberState {
party: bob_party,
group: bob_group,
};
let bad_params = LeafNodeParameters::builder()
.with_capabilities(
Capabilities::builder()
.extensions(vec![
ExtensionType::Unknown(0xf001),
ExtensionType::Unknown(0xf002),
])
.build(),
)
.build();
bob.group
.propose_self_update(
&bob.party.provider,
&bob.party.signer,
bad_params,
)
.expect_err(
"proposal creation should fail with validation error due to unsupported group context extensions (valn0602)"
);
}
#[openmls_test]
#[ignore]
fn join_rejects_unsupported_group_context_extension() {
let alice_party = PartyState::<Provider>::generate("alice", ciphersuite);
let bob_party = PartyState::<Provider>::generate("bob", ciphersuite);
let gc_extensions = Extensions::single(Extension::Unknown(
0x4141,
crate::extensions::UnknownExtension(vec![0x01]),
))
.expect("unknown extensions should be considered valid in group context");
let alice_group = MlsGroup::builder()
.ciphersuite(ciphersuite)
.with_wire_format_policy(WireFormatPolicy::new(
OutgoingWireFormatPolicy::AlwaysPlaintext,
IncomingWireFormatPolicy::Mixed,
))
.with_group_context_extensions(gc_extensions)
.build(
&alice_party.provider,
&alice_party.signer,
alice_party.credential_with_key.clone(),
)
.expect("error creating group using builder");
let mut alice = MemberState {
party: alice_party,
group: alice_group,
};
let bob_key_package = bob_party.key_package(ciphersuite, |builder| builder);
alice.propose_add_member(bob_key_package.key_package());
let (_, Some(welcome), _) = alice.commit_and_merge_pending() else {
panic!("expected receiving a welcome")
};
let welcome: MlsMessageIn = welcome.into();
let welcome = welcome
.into_welcome()
.expect("expected message to be a welcome");
if let Ok(staged) = StagedWelcome::new_from_welcome(
&bob_party.provider,
alice.group.configuration(),
welcome,
Some(alice.group.export_ratchet_tree().into()),
) {
assert!(alice
.group
.extensions()
.contains(ExtensionType::Unknown(0x4141)));
assert!(!bob_party
.key_package_bundle
.key_package
.extensions()
.contains(ExtensionType::Unknown(0x4141)));
assert!(staged
.group_context()
.extensions()
.contains(ExtensionType::Unknown(0x4141)));
unreachable!("join should reject unsupported GroupContext extensions");
}
}