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