• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 // Copyright by contributors to this project.
3 // SPDX-License-Identifier: (Apache-2.0 OR MIT)
4 
5 #[cfg(feature = "by_ref_proposal")]
6 use alloc::{vec, vec::Vec};
7 
8 use crate::{
9     client::MlsError,
10     crypto::SignaturePublicKey,
11     group::{GroupContext, PublicMessage, Sender},
12     signer::Signable,
13     tree_kem::{node::LeafIndex, TreeKemPublic},
14     CipherSuiteProvider,
15 };
16 
17 #[cfg(feature = "by_ref_proposal")]
18 use crate::{extension::ExternalSendersExt, identity::SigningIdentity};
19 
20 use super::{
21     key_schedule::KeySchedule,
22     message_signature::{AuthenticatedContent, MessageSigningContext},
23     state::GroupState,
24 };
25 
26 #[cfg(feature = "by_ref_proposal")]
27 use super::proposal::Proposal;
28 
29 #[derive(Debug)]
30 pub(crate) enum SignaturePublicKeysContainer<'a> {
31     RatchetTree(&'a TreeKemPublic),
32     #[cfg(feature = "private_message")]
33     List(&'a [Option<SignaturePublicKey>]),
34 }
35 
36 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
verify_plaintext_authentication<P: CipherSuiteProvider>( cipher_suite_provider: &P, plaintext: PublicMessage, key_schedule: Option<&KeySchedule>, state: &GroupState, ) -> Result<AuthenticatedContent, MlsError>37 pub(crate) async fn verify_plaintext_authentication<P: CipherSuiteProvider>(
38     cipher_suite_provider: &P,
39     plaintext: PublicMessage,
40     key_schedule: Option<&KeySchedule>,
41     state: &GroupState,
42 ) -> Result<AuthenticatedContent, MlsError> {
43     let tag = plaintext.membership_tag.clone();
44     let auth_content = AuthenticatedContent::from(plaintext);
45     let context = &state.context;
46 
47     #[cfg(feature = "by_ref_proposal")]
48     let external_signers = external_signers(context);
49 
50     let current_tree = &state.public_tree;
51 
52     // Verify the membership tag if needed
53     match &auth_content.content.sender {
54         Sender::Member(_) => {
55             if let Some(key_schedule) = key_schedule {
56                 let expected_tag = &key_schedule
57                     .get_membership_tag(&auth_content, context, cipher_suite_provider)
58                     .await?;
59 
60                 let plaintext_tag = tag.as_ref().ok_or(MlsError::InvalidMembershipTag)?;
61 
62                 if expected_tag != plaintext_tag {
63                     return Err(MlsError::InvalidMembershipTag);
64                 }
65             }
66         }
67         _ => {
68             tag.is_none()
69                 .then_some(())
70                 .ok_or(MlsError::MembershipTagForNonMember)?;
71         }
72     }
73 
74     // Verify that the signature on the MLSAuthenticatedContent verifies using the public key
75     // from the credential stored at the leaf in the tree indicated by the sender field.
76     verify_auth_content_signature(
77         cipher_suite_provider,
78         SignaturePublicKeysContainer::RatchetTree(current_tree),
79         context,
80         &auth_content,
81         #[cfg(feature = "by_ref_proposal")]
82         &external_signers,
83     )
84     .await?;
85 
86     Ok(auth_content)
87 }
88 
89 #[cfg(feature = "by_ref_proposal")]
external_signers(context: &GroupContext) -> Vec<SigningIdentity>90 fn external_signers(context: &GroupContext) -> Vec<SigningIdentity> {
91     context
92         .extensions
93         .get_as::<ExternalSendersExt>()
94         .unwrap_or(None)
95         .map_or(vec![], |extern_senders_ext| {
96             extern_senders_ext.allowed_senders
97         })
98 }
99 
100 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
verify_auth_content_signature<P: CipherSuiteProvider>( cipher_suite_provider: &P, signature_keys_container: SignaturePublicKeysContainer<'_>, context: &GroupContext, auth_content: &AuthenticatedContent, #[cfg(feature = "by_ref_proposal")] external_signers: &[SigningIdentity], ) -> Result<(), MlsError>101 pub(crate) async fn verify_auth_content_signature<P: CipherSuiteProvider>(
102     cipher_suite_provider: &P,
103     signature_keys_container: SignaturePublicKeysContainer<'_>,
104     context: &GroupContext,
105     auth_content: &AuthenticatedContent,
106     #[cfg(feature = "by_ref_proposal")] external_signers: &[SigningIdentity],
107 ) -> Result<(), MlsError> {
108     let sender_public_key = signing_identity_for_sender(
109         signature_keys_container,
110         &auth_content.content.sender,
111         &auth_content.content.content,
112         #[cfg(feature = "by_ref_proposal")]
113         external_signers,
114     )?;
115 
116     let context = MessageSigningContext {
117         group_context: Some(context),
118         protocol_version: context.protocol_version,
119     };
120 
121     auth_content
122         .verify(cipher_suite_provider, &sender_public_key, &context)
123         .await?;
124 
125     Ok(())
126 }
127 
signing_identity_for_sender( signature_keys_container: SignaturePublicKeysContainer, sender: &Sender, content: &super::framing::Content, #[cfg(feature = "by_ref_proposal")] external_signers: &[SigningIdentity], ) -> Result<SignaturePublicKey, MlsError>128 fn signing_identity_for_sender(
129     signature_keys_container: SignaturePublicKeysContainer,
130     sender: &Sender,
131     content: &super::framing::Content,
132     #[cfg(feature = "by_ref_proposal")] external_signers: &[SigningIdentity],
133 ) -> Result<SignaturePublicKey, MlsError> {
134     match sender {
135         Sender::Member(leaf_index) => {
136             signing_identity_for_member(signature_keys_container, LeafIndex(*leaf_index))
137         }
138         #[cfg(feature = "by_ref_proposal")]
139         Sender::External(external_key_index) => {
140             signing_identity_for_external(*external_key_index, external_signers)
141         }
142         Sender::NewMemberCommit => signing_identity_for_new_member_commit(content),
143         #[cfg(feature = "by_ref_proposal")]
144         Sender::NewMemberProposal => signing_identity_for_new_member_proposal(content),
145     }
146 }
147 
signing_identity_for_member( signature_keys_container: SignaturePublicKeysContainer, leaf_index: LeafIndex, ) -> Result<SignaturePublicKey, MlsError>148 fn signing_identity_for_member(
149     signature_keys_container: SignaturePublicKeysContainer,
150     leaf_index: LeafIndex,
151 ) -> Result<SignaturePublicKey, MlsError> {
152     match signature_keys_container {
153         SignaturePublicKeysContainer::RatchetTree(tree) => Ok(tree
154             .get_leaf_node(leaf_index)?
155             .signing_identity
156             .signature_key
157             .clone()), // TODO: We can probably get rid of this clone
158         #[cfg(feature = "private_message")]
159         SignaturePublicKeysContainer::List(list) => list
160             .get(leaf_index.0 as usize)
161             .cloned()
162             .flatten()
163             .ok_or(MlsError::LeafNotFound(*leaf_index)),
164     }
165 }
166 
167 #[cfg(feature = "by_ref_proposal")]
signing_identity_for_external( index: u32, external_signers: &[SigningIdentity], ) -> Result<SignaturePublicKey, MlsError>168 fn signing_identity_for_external(
169     index: u32,
170     external_signers: &[SigningIdentity],
171 ) -> Result<SignaturePublicKey, MlsError> {
172     external_signers
173         .get(index as usize)
174         .map(|spk| spk.signature_key.clone())
175         .ok_or(MlsError::UnknownSigningIdentityForExternalSender)
176 }
177 
signing_identity_for_new_member_commit( content: &super::framing::Content, ) -> Result<SignaturePublicKey, MlsError>178 fn signing_identity_for_new_member_commit(
179     content: &super::framing::Content,
180 ) -> Result<SignaturePublicKey, MlsError> {
181     match content {
182         super::framing::Content::Commit(commit) => {
183             if let Some(path) = &commit.path {
184                 Ok(path.leaf_node.signing_identity.signature_key.clone())
185             } else {
186                 Err(MlsError::CommitMissingPath)
187             }
188         }
189         #[cfg(any(feature = "private_message", feature = "by_ref_proposal"))]
190         _ => Err(MlsError::ExpectedCommitForNewMemberCommit),
191     }
192 }
193 
194 #[cfg(feature = "by_ref_proposal")]
signing_identity_for_new_member_proposal( content: &super::framing::Content, ) -> Result<SignaturePublicKey, MlsError>195 fn signing_identity_for_new_member_proposal(
196     content: &super::framing::Content,
197 ) -> Result<SignaturePublicKey, MlsError> {
198     match content {
199         super::framing::Content::Proposal(proposal) => {
200             if let Proposal::Add(p) = proposal.as_ref() {
201                 Ok(p.key_package
202                     .leaf_node
203                     .signing_identity
204                     .signature_key
205                     .clone())
206             } else {
207                 Err(MlsError::ExpectedAddProposalForNewMemberProposal)
208             }
209         }
210         _ => Err(MlsError::ExpectedAddProposalForNewMemberProposal),
211     }
212 }
213 
214 #[cfg(test)]
215 mod tests {
216     use crate::{
217         client::{
218             test_utils::{test_client_with_key_pkg, TEST_CIPHER_SUITE, TEST_PROTOCOL_VERSION},
219             MlsError,
220         },
221         client_builder::test_utils::TestClientConfig,
222         crypto::test_utils::test_cipher_suite_provider,
223         group::{
224             membership_tag::MembershipTag,
225             message_signature::{AuthenticatedContent, MessageSignature},
226             test_utils::{test_group_custom, TestGroup},
227             Group, PublicMessage,
228         },
229         tree_kem::node::LeafIndex,
230     };
231     use alloc::vec;
232     use assert_matches::assert_matches;
233 
234     #[cfg(feature = "by_ref_proposal")]
235     use crate::{extension::ExternalSendersExt, ExtensionList};
236 
237     #[cfg(feature = "by_ref_proposal")]
238     use crate::{
239         crypto::SignatureSecretKey,
240         group::{
241             message_signature::MessageSigningContext,
242             proposal::{AddProposal, Proposal, RemoveProposal},
243             Content,
244         },
245         key_package::KeyPackageGeneration,
246         signer::Signable,
247         WireFormat,
248     };
249 
250     #[cfg(feature = "by_ref_proposal")]
251     use alloc::boxed::Box;
252 
253     use crate::group::{
254         test_utils::{test_group, test_member},
255         Sender,
256     };
257 
258     #[cfg(feature = "by_ref_proposal")]
259     use crate::identity::test_utils::get_test_signing_identity;
260 
261     use super::{verify_auth_content_signature, verify_plaintext_authentication};
262 
263     #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
make_signed_plaintext(group: &mut Group<TestClientConfig>) -> PublicMessage264     async fn make_signed_plaintext(group: &mut Group<TestClientConfig>) -> PublicMessage {
265         group
266             .commit(vec![])
267             .await
268             .unwrap()
269             .commit_message
270             .into_plaintext()
271             .unwrap()
272     }
273 
274     struct TestEnv {
275         alice: TestGroup,
276         bob: TestGroup,
277     }
278 
279     impl TestEnv {
280         #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
new() -> Self281         async fn new() -> Self {
282             let mut alice = test_group_custom(
283                 TEST_PROTOCOL_VERSION,
284                 TEST_CIPHER_SUITE,
285                 Default::default(),
286                 None,
287                 None,
288             )
289             .await;
290 
291             let (bob_client, bob_key_pkg) =
292                 test_client_with_key_pkg(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "bob").await;
293 
294             let commit_output = alice
295                 .group
296                 .commit_builder()
297                 .add_member(bob_key_pkg)
298                 .unwrap()
299                 .build()
300                 .await
301                 .unwrap();
302 
303             alice.group.apply_pending_commit().await.unwrap();
304 
305             let (bob, _) = Group::join(
306                 &commit_output.welcome_messages[0],
307                 None,
308                 bob_client.config,
309                 bob_client.signer.unwrap(),
310             )
311             .await
312             .unwrap();
313 
314             TestEnv {
315                 alice,
316                 bob: TestGroup { group: bob },
317             }
318         }
319     }
320 
321     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
valid_plaintext_is_verified()322     async fn valid_plaintext_is_verified() {
323         let mut env = TestEnv::new().await;
324 
325         let message = make_signed_plaintext(&mut env.alice.group).await;
326 
327         verify_plaintext_authentication(
328             &env.bob.group.cipher_suite_provider,
329             message,
330             Some(&env.bob.group.key_schedule),
331             &env.bob.group.state,
332         )
333         .await
334         .unwrap();
335     }
336 
337     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
valid_auth_content_is_verified()338     async fn valid_auth_content_is_verified() {
339         let mut env = TestEnv::new().await;
340 
341         let message = AuthenticatedContent::from(make_signed_plaintext(&mut env.alice.group).await);
342 
343         verify_auth_content_signature(
344             &env.bob.group.cipher_suite_provider,
345             super::SignaturePublicKeysContainer::RatchetTree(&env.bob.group.state.public_tree),
346             env.bob.group.context(),
347             &message,
348             #[cfg(feature = "by_ref_proposal")]
349             &[],
350         )
351         .await
352         .unwrap();
353     }
354 
355     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
invalid_plaintext_is_not_verified()356     async fn invalid_plaintext_is_not_verified() {
357         let mut env = TestEnv::new().await;
358         let mut message = make_signed_plaintext(&mut env.alice.group).await;
359         message.auth.signature = MessageSignature::from(b"test".to_vec());
360 
361         message.membership_tag = env
362             .alice
363             .group
364             .key_schedule
365             .get_membership_tag(
366                 &AuthenticatedContent::from(message.clone()),
367                 env.alice.group.context(),
368                 &test_cipher_suite_provider(env.alice.group.cipher_suite()),
369             )
370             .await
371             .unwrap()
372             .into();
373 
374         let res = verify_plaintext_authentication(
375             &env.bob.group.cipher_suite_provider,
376             message,
377             Some(&env.bob.group.key_schedule),
378             &env.bob.group.state,
379         )
380         .await;
381 
382         assert_matches!(res, Err(MlsError::InvalidSignature));
383     }
384 
385     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
plaintext_from_member_requires_membership_tag()386     async fn plaintext_from_member_requires_membership_tag() {
387         let mut env = TestEnv::new().await;
388         let mut message = make_signed_plaintext(&mut env.alice.group).await;
389         message.membership_tag = None;
390 
391         let res = verify_plaintext_authentication(
392             &env.bob.group.cipher_suite_provider,
393             message,
394             Some(&env.bob.group.key_schedule),
395             &env.bob.group.state,
396         )
397         .await;
398 
399         assert_matches!(res, Err(MlsError::InvalidMembershipTag));
400     }
401 
402     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
plaintext_fails_with_invalid_membership_tag()403     async fn plaintext_fails_with_invalid_membership_tag() {
404         let mut env = TestEnv::new().await;
405         let mut message = make_signed_plaintext(&mut env.alice.group).await;
406         message.membership_tag = Some(MembershipTag::from(b"test".to_vec()));
407 
408         let res = verify_plaintext_authentication(
409             &env.bob.group.cipher_suite_provider,
410             message,
411             Some(&env.bob.group.key_schedule),
412             &env.bob.group.state,
413         )
414         .await;
415 
416         assert_matches!(res, Err(MlsError::InvalidMembershipTag));
417     }
418 
419     #[cfg(feature = "by_ref_proposal")]
420     #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
test_new_member_proposal<F>( key_pkg_gen: KeyPackageGeneration, signer: &SignatureSecretKey, test_group: &TestGroup, mut edit: F, ) -> PublicMessage where F: FnMut(&mut AuthenticatedContent),421     async fn test_new_member_proposal<F>(
422         key_pkg_gen: KeyPackageGeneration,
423         signer: &SignatureSecretKey,
424         test_group: &TestGroup,
425         mut edit: F,
426     ) -> PublicMessage
427     where
428         F: FnMut(&mut AuthenticatedContent),
429     {
430         let mut content = AuthenticatedContent::new_signed(
431             &test_group.group.cipher_suite_provider,
432             test_group.group.context(),
433             Sender::NewMemberProposal,
434             Content::Proposal(Box::new(Proposal::Add(Box::new(AddProposal {
435                 key_package: key_pkg_gen.key_package,
436             })))),
437             signer,
438             WireFormat::PublicMessage,
439             vec![],
440         )
441         .await
442         .unwrap();
443 
444         edit(&mut content);
445 
446         let signing_context = MessageSigningContext {
447             group_context: Some(test_group.group.context()),
448             protocol_version: test_group.group.protocol_version(),
449         };
450 
451         content
452             .sign(
453                 &test_group.group.cipher_suite_provider,
454                 signer,
455                 &signing_context,
456             )
457             .await
458             .unwrap();
459 
460         PublicMessage {
461             content: content.content,
462             auth: content.auth,
463             membership_tag: None,
464         }
465     }
466 
467     #[cfg(feature = "by_ref_proposal")]
468     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
valid_proposal_from_new_member_is_verified()469     async fn valid_proposal_from_new_member_is_verified() {
470         let test_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
471         let (key_pkg_gen, signer) =
472             test_member(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, b"bob").await;
473         let message = test_new_member_proposal(key_pkg_gen, &signer, &test_group, |_| {}).await;
474 
475         verify_plaintext_authentication(
476             &test_group.group.cipher_suite_provider,
477             message,
478             Some(&test_group.group.key_schedule),
479             &test_group.group.state,
480         )
481         .await
482         .unwrap();
483     }
484 
485     #[cfg(feature = "by_ref_proposal")]
486     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
proposal_from_new_member_must_not_have_membership_tag()487     async fn proposal_from_new_member_must_not_have_membership_tag() {
488         let test_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
489         let (key_pkg_gen, signer) =
490             test_member(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, b"bob").await;
491 
492         let mut message = test_new_member_proposal(key_pkg_gen, &signer, &test_group, |_| {}).await;
493         message.membership_tag = Some(MembershipTag::from(vec![]));
494 
495         let res = verify_plaintext_authentication(
496             &test_group.group.cipher_suite_provider,
497             message,
498             Some(&test_group.group.key_schedule),
499             &test_group.group.state,
500         )
501         .await;
502 
503         assert_matches!(res, Err(MlsError::MembershipTagForNonMember));
504     }
505 
506     #[cfg(feature = "by_ref_proposal")]
507     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
new_member_proposal_sender_must_be_add_proposal()508     async fn new_member_proposal_sender_must_be_add_proposal() {
509         let test_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
510         let (key_pkg_gen, signer) =
511             test_member(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, b"bob").await;
512 
513         let message = test_new_member_proposal(key_pkg_gen, &signer, &test_group, |msg| {
514             msg.content.content = Content::Proposal(Box::new(Proposal::Remove(RemoveProposal {
515                 to_remove: LeafIndex(0),
516             })))
517         })
518         .await;
519 
520         let res: Result<AuthenticatedContent, MlsError> = verify_plaintext_authentication(
521             &test_group.group.cipher_suite_provider,
522             message,
523             Some(&test_group.group.key_schedule),
524             &test_group.group.state,
525         )
526         .await;
527 
528         assert_matches!(res, Err(MlsError::ExpectedAddProposalForNewMemberProposal));
529     }
530 
531     #[cfg(feature = "by_ref_proposal")]
532     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
new_member_commit_must_be_external_commit()533     async fn new_member_commit_must_be_external_commit() {
534         let test_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
535         let (key_pkg_gen, signer) =
536             test_member(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, b"bob").await;
537 
538         let message = test_new_member_proposal(key_pkg_gen, &signer, &test_group, |msg| {
539             msg.content.sender = Sender::NewMemberCommit;
540         })
541         .await;
542 
543         let res = verify_plaintext_authentication(
544             &test_group.group.cipher_suite_provider,
545             message,
546             Some(&test_group.group.key_schedule),
547             &test_group.group.state,
548         )
549         .await;
550 
551         assert_matches!(res, Err(MlsError::ExpectedCommitForNewMemberCommit));
552     }
553 
554     #[cfg(feature = "by_ref_proposal")]
555     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
valid_proposal_from_external_is_verified()556     async fn valid_proposal_from_external_is_verified() {
557         let (bob_key_pkg_gen, _) =
558             test_member(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, b"bob").await;
559 
560         let (ted_signing, ted_secret) = get_test_signing_identity(TEST_CIPHER_SUITE, b"ted").await;
561 
562         let mut test_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
563         let mut extensions = ExtensionList::default();
564 
565         extensions
566             .set_from(ExternalSendersExt {
567                 allowed_senders: vec![ted_signing],
568             })
569             .unwrap();
570 
571         test_group
572             .group
573             .commit_builder()
574             .set_group_context_ext(extensions)
575             .unwrap()
576             .build()
577             .await
578             .unwrap();
579 
580         test_group.group.apply_pending_commit().await.unwrap();
581 
582         let message = test_new_member_proposal(bob_key_pkg_gen, &ted_secret, &test_group, |msg| {
583             msg.content.sender = Sender::External(0)
584         })
585         .await;
586 
587         verify_plaintext_authentication(
588             &test_group.group.cipher_suite_provider,
589             message,
590             Some(&test_group.group.key_schedule),
591             &test_group.group.state,
592         )
593         .await
594         .unwrap();
595     }
596 
597     #[cfg(feature = "by_ref_proposal")]
598     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
external_proposal_must_be_from_valid_sender()599     async fn external_proposal_must_be_from_valid_sender() {
600         let (bob_key_pkg_gen, _) =
601             test_member(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, b"bob").await;
602         let (_, ted_secret) = get_test_signing_identity(TEST_CIPHER_SUITE, b"ted").await;
603         let test_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
604 
605         let message = test_new_member_proposal(bob_key_pkg_gen, &ted_secret, &test_group, |msg| {
606             msg.content.sender = Sender::External(0)
607         })
608         .await;
609 
610         let res = verify_plaintext_authentication(
611             &test_group.group.cipher_suite_provider,
612             message,
613             Some(&test_group.group.key_schedule),
614             &test_group.group.state,
615         )
616         .await;
617 
618         assert_matches!(res, Err(MlsError::UnknownSigningIdentityForExternalSender));
619     }
620 
621     #[cfg(feature = "by_ref_proposal")]
622     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
proposal_from_external_sender_must_not_have_membership_tag()623     async fn proposal_from_external_sender_must_not_have_membership_tag() {
624         let (bob_key_pkg_gen, _) =
625             test_member(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, b"bob").await;
626 
627         let (_, ted_secret) = get_test_signing_identity(TEST_CIPHER_SUITE, b"ted").await;
628 
629         let test_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
630 
631         let mut message =
632             test_new_member_proposal(bob_key_pkg_gen, &ted_secret, &test_group, |_| {}).await;
633 
634         message.membership_tag = Some(MembershipTag::from(vec![]));
635 
636         let res = verify_plaintext_authentication(
637             &test_group.group.cipher_suite_provider,
638             message,
639             Some(&test_group.group.key_schedule),
640             &test_group.group.state,
641         )
642         .await;
643 
644         assert_matches!(res, Err(MlsError::MembershipTagForNonMember));
645     }
646 }
647