// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // Copyright by contributors to this project. // SPDX-License-Identifier: (Apache-2.0 OR MIT) #[cfg(feature = "by_ref_proposal")] use alloc::{vec, vec::Vec}; use crate::{ client::MlsError, crypto::SignaturePublicKey, group::{GroupContext, PublicMessage, Sender}, signer::Signable, tree_kem::{node::LeafIndex, TreeKemPublic}, CipherSuiteProvider, }; #[cfg(feature = "by_ref_proposal")] use crate::{extension::ExternalSendersExt, identity::SigningIdentity}; use super::{ key_schedule::KeySchedule, message_signature::{AuthenticatedContent, MessageSigningContext}, state::GroupState, }; #[cfg(feature = "by_ref_proposal")] use super::proposal::Proposal; #[derive(Debug)] pub(crate) enum SignaturePublicKeysContainer<'a> { RatchetTree(&'a TreeKemPublic), #[cfg(feature = "private_message")] List(&'a [Option]), } #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub(crate) async fn verify_plaintext_authentication( cipher_suite_provider: &P, plaintext: PublicMessage, key_schedule: Option<&KeySchedule>, self_index: Option, state: &GroupState, ) -> Result { let tag = plaintext.membership_tag.clone(); let auth_content = AuthenticatedContent::from(plaintext); let context = &state.context; #[cfg(feature = "by_ref_proposal")] let external_signers = external_signers(context); let current_tree = &state.public_tree; // Verify the membership tag if needed match &auth_content.content.sender { Sender::Member(index) => { if let Some(key_schedule) = key_schedule { let expected_tag = &key_schedule .get_membership_tag(&auth_content, context, cipher_suite_provider) .await?; let plaintext_tag = tag.as_ref().ok_or(MlsError::InvalidMembershipTag)?; if expected_tag != plaintext_tag { return Err(MlsError::InvalidMembershipTag); } } if self_index == Some(LeafIndex(*index)) { return Err(MlsError::CantProcessMessageFromSelf); } } _ => { tag.is_none() .then_some(()) .ok_or(MlsError::MembershipTagForNonMember)?; } } // Verify that the signature on the MLSAuthenticatedContent verifies using the public key // from the credential stored at the leaf in the tree indicated by the sender field. verify_auth_content_signature( cipher_suite_provider, SignaturePublicKeysContainer::RatchetTree(current_tree), context, &auth_content, #[cfg(feature = "by_ref_proposal")] &external_signers, ) .await?; Ok(auth_content) } #[cfg(feature = "by_ref_proposal")] fn external_signers(context: &GroupContext) -> Vec { context .extensions .get_as::() .unwrap_or(None) .map_or(vec![], |extern_senders_ext| { extern_senders_ext.allowed_senders }) } #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub(crate) async fn verify_auth_content_signature( cipher_suite_provider: &P, signature_keys_container: SignaturePublicKeysContainer<'_>, context: &GroupContext, auth_content: &AuthenticatedContent, #[cfg(feature = "by_ref_proposal")] external_signers: &[SigningIdentity], ) -> Result<(), MlsError> { let sender_public_key = signing_identity_for_sender( signature_keys_container, &auth_content.content.sender, &auth_content.content.content, #[cfg(feature = "by_ref_proposal")] external_signers, )?; let context = MessageSigningContext { group_context: Some(context), protocol_version: context.protocol_version, }; auth_content .verify(cipher_suite_provider, &sender_public_key, &context) .await?; Ok(()) } fn signing_identity_for_sender( signature_keys_container: SignaturePublicKeysContainer, sender: &Sender, content: &super::framing::Content, #[cfg(feature = "by_ref_proposal")] external_signers: &[SigningIdentity], ) -> Result { match sender { Sender::Member(leaf_index) => { signing_identity_for_member(signature_keys_container, LeafIndex(*leaf_index)) } #[cfg(feature = "by_ref_proposal")] Sender::External(external_key_index) => { signing_identity_for_external(*external_key_index, external_signers) } Sender::NewMemberCommit => signing_identity_for_new_member_commit(content), #[cfg(feature = "by_ref_proposal")] Sender::NewMemberProposal => signing_identity_for_new_member_proposal(content), } } fn signing_identity_for_member( signature_keys_container: SignaturePublicKeysContainer, leaf_index: LeafIndex, ) -> Result { match signature_keys_container { SignaturePublicKeysContainer::RatchetTree(tree) => Ok(tree .get_leaf_node(leaf_index)? .signing_identity .signature_key .clone()), // TODO: We can probably get rid of this clone #[cfg(feature = "private_message")] SignaturePublicKeysContainer::List(list) => list .get(leaf_index.0 as usize) .cloned() .flatten() .ok_or(MlsError::LeafNotFound(*leaf_index)), } } #[cfg(feature = "by_ref_proposal")] fn signing_identity_for_external( index: u32, external_signers: &[SigningIdentity], ) -> Result { external_signers .get(index as usize) .map(|spk| spk.signature_key.clone()) .ok_or(MlsError::UnknownSigningIdentityForExternalSender) } fn signing_identity_for_new_member_commit( content: &super::framing::Content, ) -> Result { match content { super::framing::Content::Commit(commit) => { if let Some(path) = &commit.path { Ok(path.leaf_node.signing_identity.signature_key.clone()) } else { Err(MlsError::CommitMissingPath) } } #[cfg(any(feature = "private_message", feature = "by_ref_proposal"))] _ => Err(MlsError::ExpectedCommitForNewMemberCommit), } } #[cfg(feature = "by_ref_proposal")] fn signing_identity_for_new_member_proposal( content: &super::framing::Content, ) -> Result { match content { super::framing::Content::Proposal(proposal) => { if let Proposal::Add(p) = proposal.as_ref() { Ok(p.key_package .leaf_node .signing_identity .signature_key .clone()) } else { Err(MlsError::ExpectedAddProposalForNewMemberProposal) } } _ => Err(MlsError::ExpectedAddProposalForNewMemberProposal), } } #[cfg(test)] mod tests { use crate::{ client::{ test_utils::{test_client_with_key_pkg, TEST_CIPHER_SUITE, TEST_PROTOCOL_VERSION}, MlsError, }, client_builder::test_utils::TestClientConfig, crypto::test_utils::test_cipher_suite_provider, group::{ membership_tag::MembershipTag, message_signature::{AuthenticatedContent, MessageSignature}, test_utils::{test_group_custom, TestGroup}, Group, PublicMessage, }, tree_kem::node::LeafIndex, }; use alloc::vec; use assert_matches::assert_matches; #[cfg(feature = "by_ref_proposal")] use crate::{extension::ExternalSendersExt, ExtensionList}; #[cfg(feature = "by_ref_proposal")] use crate::{ crypto::SignatureSecretKey, group::{ message_signature::MessageSigningContext, proposal::{AddProposal, Proposal, RemoveProposal}, Content, }, key_package::KeyPackageGeneration, signer::Signable, WireFormat, }; #[cfg(feature = "by_ref_proposal")] use alloc::boxed::Box; use crate::group::{ test_utils::{test_group, test_member}, Sender, }; #[cfg(feature = "by_ref_proposal")] use crate::identity::test_utils::get_test_signing_identity; use super::{verify_auth_content_signature, verify_plaintext_authentication}; #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] async fn make_signed_plaintext(group: &mut Group) -> PublicMessage { group .commit(vec![]) .await .unwrap() .commit_message .into_plaintext() .unwrap() } struct TestEnv { alice: TestGroup, bob: TestGroup, } impl TestEnv { #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] async fn new() -> Self { let mut alice = test_group_custom( TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, Default::default(), None, None, ) .await; let (bob_client, bob_key_pkg) = test_client_with_key_pkg(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "bob").await; let commit_output = alice .group .commit_builder() .add_member(bob_key_pkg) .unwrap() .build() .await .unwrap(); alice.group.apply_pending_commit().await.unwrap(); let (bob, _) = Group::join( &commit_output.welcome_messages[0], None, bob_client.config, bob_client.signer.unwrap(), ) .await .unwrap(); TestEnv { alice, bob: TestGroup { group: bob }, } } } #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn valid_plaintext_is_verified() { let mut env = TestEnv::new().await; let message = make_signed_plaintext(&mut env.alice.group).await; verify_plaintext_authentication( &env.bob.group.cipher_suite_provider, message, Some(&env.bob.group.key_schedule), None, &env.bob.group.state, ) .await .unwrap(); } #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn valid_auth_content_is_verified() { let mut env = TestEnv::new().await; let message = AuthenticatedContent::from(make_signed_plaintext(&mut env.alice.group).await); verify_auth_content_signature( &env.bob.group.cipher_suite_provider, super::SignaturePublicKeysContainer::RatchetTree(&env.bob.group.state.public_tree), env.bob.group.context(), &message, #[cfg(feature = "by_ref_proposal")] &[], ) .await .unwrap(); } #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn invalid_plaintext_is_not_verified() { let mut env = TestEnv::new().await; let mut message = make_signed_plaintext(&mut env.alice.group).await; message.auth.signature = MessageSignature::from(b"test".to_vec()); message.membership_tag = env .alice .group .key_schedule .get_membership_tag( &AuthenticatedContent::from(message.clone()), env.alice.group.context(), &test_cipher_suite_provider(env.alice.group.cipher_suite()), ) .await .unwrap() .into(); let res = verify_plaintext_authentication( &env.bob.group.cipher_suite_provider, message, Some(&env.bob.group.key_schedule), None, &env.bob.group.state, ) .await; assert_matches!(res, Err(MlsError::InvalidSignature)); } #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn plaintext_from_member_requires_membership_tag() { let mut env = TestEnv::new().await; let mut message = make_signed_plaintext(&mut env.alice.group).await; message.membership_tag = None; let res = verify_plaintext_authentication( &env.bob.group.cipher_suite_provider, message, Some(&env.bob.group.key_schedule), None, &env.bob.group.state, ) .await; assert_matches!(res, Err(MlsError::InvalidMembershipTag)); } #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn plaintext_fails_with_invalid_membership_tag() { let mut env = TestEnv::new().await; let mut message = make_signed_plaintext(&mut env.alice.group).await; message.membership_tag = Some(MembershipTag::from(b"test".to_vec())); let res = verify_plaintext_authentication( &env.bob.group.cipher_suite_provider, message, Some(&env.bob.group.key_schedule), None, &env.bob.group.state, ) .await; assert_matches!(res, Err(MlsError::InvalidMembershipTag)); } #[cfg(feature = "by_ref_proposal")] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] async fn test_new_member_proposal( key_pkg_gen: KeyPackageGeneration, signer: &SignatureSecretKey, test_group: &TestGroup, mut edit: F, ) -> PublicMessage where F: FnMut(&mut AuthenticatedContent), { let mut content = AuthenticatedContent::new_signed( &test_group.group.cipher_suite_provider, test_group.group.context(), Sender::NewMemberProposal, Content::Proposal(Box::new(Proposal::Add(Box::new(AddProposal { key_package: key_pkg_gen.key_package, })))), signer, WireFormat::PublicMessage, vec![], ) .await .unwrap(); edit(&mut content); let signing_context = MessageSigningContext { group_context: Some(test_group.group.context()), protocol_version: test_group.group.protocol_version(), }; content .sign( &test_group.group.cipher_suite_provider, signer, &signing_context, ) .await .unwrap(); PublicMessage { content: content.content, auth: content.auth, membership_tag: None, } } #[cfg(feature = "by_ref_proposal")] #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn valid_proposal_from_new_member_is_verified() { let test_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; let (key_pkg_gen, signer) = test_member(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, b"bob").await; let message = test_new_member_proposal(key_pkg_gen, &signer, &test_group, |_| {}).await; verify_plaintext_authentication( &test_group.group.cipher_suite_provider, message, Some(&test_group.group.key_schedule), None, &test_group.group.state, ) .await .unwrap(); } #[cfg(feature = "by_ref_proposal")] #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn proposal_from_new_member_must_not_have_membership_tag() { let test_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; let (key_pkg_gen, signer) = test_member(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, b"bob").await; let mut message = test_new_member_proposal(key_pkg_gen, &signer, &test_group, |_| {}).await; message.membership_tag = Some(MembershipTag::from(vec![])); let res = verify_plaintext_authentication( &test_group.group.cipher_suite_provider, message, Some(&test_group.group.key_schedule), None, &test_group.group.state, ) .await; assert_matches!(res, Err(MlsError::MembershipTagForNonMember)); } #[cfg(feature = "by_ref_proposal")] #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn new_member_proposal_sender_must_be_add_proposal() { let test_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; let (key_pkg_gen, signer) = test_member(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, b"bob").await; let message = test_new_member_proposal(key_pkg_gen, &signer, &test_group, |msg| { msg.content.content = Content::Proposal(Box::new(Proposal::Remove(RemoveProposal { to_remove: LeafIndex(0), }))) }) .await; let res: Result = verify_plaintext_authentication( &test_group.group.cipher_suite_provider, message, Some(&test_group.group.key_schedule), None, &test_group.group.state, ) .await; assert_matches!(res, Err(MlsError::ExpectedAddProposalForNewMemberProposal)); } #[cfg(feature = "by_ref_proposal")] #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn new_member_commit_must_be_external_commit() { let test_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; let (key_pkg_gen, signer) = test_member(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, b"bob").await; let message = test_new_member_proposal(key_pkg_gen, &signer, &test_group, |msg| { msg.content.sender = Sender::NewMemberCommit; }) .await; let res = verify_plaintext_authentication( &test_group.group.cipher_suite_provider, message, Some(&test_group.group.key_schedule), None, &test_group.group.state, ) .await; assert_matches!(res, Err(MlsError::ExpectedCommitForNewMemberCommit)); } #[cfg(feature = "by_ref_proposal")] #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn valid_proposal_from_external_is_verified() { let (bob_key_pkg_gen, _) = test_member(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, b"bob").await; let (ted_signing, ted_secret) = get_test_signing_identity(TEST_CIPHER_SUITE, b"ted").await; let mut test_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; let mut extensions = ExtensionList::default(); extensions .set_from(ExternalSendersExt { allowed_senders: vec![ted_signing], }) .unwrap(); test_group .group .commit_builder() .set_group_context_ext(extensions) .unwrap() .build() .await .unwrap(); test_group.group.apply_pending_commit().await.unwrap(); let message = test_new_member_proposal(bob_key_pkg_gen, &ted_secret, &test_group, |msg| { msg.content.sender = Sender::External(0) }) .await; verify_plaintext_authentication( &test_group.group.cipher_suite_provider, message, Some(&test_group.group.key_schedule), None, &test_group.group.state, ) .await .unwrap(); } #[cfg(feature = "by_ref_proposal")] #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn external_proposal_must_be_from_valid_sender() { let (bob_key_pkg_gen, _) = test_member(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, b"bob").await; let (_, ted_secret) = get_test_signing_identity(TEST_CIPHER_SUITE, b"ted").await; let test_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; let message = test_new_member_proposal(bob_key_pkg_gen, &ted_secret, &test_group, |msg| { msg.content.sender = Sender::External(0) }) .await; let res = verify_plaintext_authentication( &test_group.group.cipher_suite_provider, message, Some(&test_group.group.key_schedule), None, &test_group.group.state, ) .await; assert_matches!(res, Err(MlsError::UnknownSigningIdentityForExternalSender)); } #[cfg(feature = "by_ref_proposal")] #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn proposal_from_external_sender_must_not_have_membership_tag() { let (bob_key_pkg_gen, _) = test_member(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, b"bob").await; let (_, ted_secret) = get_test_signing_identity(TEST_CIPHER_SUITE, b"ted").await; let test_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; let mut message = test_new_member_proposal(bob_key_pkg_gen, &ted_secret, &test_group, |_| {}).await; message.membership_tag = Some(MembershipTag::from(vec![])); let res = verify_plaintext_authentication( &test_group.group.cipher_suite_provider, message, Some(&test_group.group.key_schedule), None, &test_group.group.state, ) .await; assert_matches!(res, Err(MlsError::MembershipTagForNonMember)); } #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn plaintext_from_self_fails_verification() { let mut env = TestEnv::new().await; let message = make_signed_plaintext(&mut env.alice.group).await; let res = verify_plaintext_authentication( &env.alice.group.cipher_suite_provider, message, Some(&env.alice.group.key_schedule), Some(LeafIndex::new(env.alice.group.current_member_index())), &env.alice.group.state, ) .await; assert_matches!(res, Err(MlsError::CantProcessMessageFromSelf)) } }