mls_rs/group/
mod.rs

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
5use alloc::vec;
6use alloc::vec::Vec;
7use core::fmt::{self, Debug};
8use mls_rs_codec::{MlsDecode, MlsEncode, MlsSize};
9use mls_rs_core::error::IntoAnyError;
10#[cfg(feature = "last_resort_key_package_ext")]
11use mls_rs_core::extension::MlsExtension;
12use mls_rs_core::identity::MemberValidationContext;
13use mls_rs_core::secret::Secret;
14use mls_rs_core::time::MlsTime;
15use snapshot::PendingCommitSnapshot;
16
17use crate::cipher_suite::CipherSuite;
18use crate::client::MlsError;
19use crate::client_config::ClientConfig;
20use crate::crypto::{HpkeCiphertext, SignatureSecretKey};
21#[cfg(feature = "last_resort_key_package_ext")]
22use crate::extension::LastResortKeyPackageExt;
23use crate::extension::RatchetTreeExt;
24use crate::identity::SigningIdentity;
25use crate::key_package::{KeyPackage, KeyPackageGeneration, KeyPackageRef};
26use crate::protocol_version::ProtocolVersion;
27use crate::psk::secret::PskSecret;
28use crate::psk::PreSharedKeyID;
29use crate::signer::Signable;
30use crate::tree_kem::hpke_encryption::HpkeEncryptable;
31use crate::tree_kem::kem::TreeKem;
32use crate::tree_kem::leaf_node::LeafNode;
33use crate::tree_kem::leaf_node_validator::{LeafNodeValidator, ValidationContext};
34use crate::tree_kem::node::LeafIndex;
35use crate::tree_kem::path_secret::PathSecret;
36pub use crate::tree_kem::Capabilities;
37use crate::tree_kem::{math as tree_math, ValidatedUpdatePath};
38use crate::tree_kem::{TreeKemPrivate, TreeKemPublic};
39use crate::{CipherSuiteProvider, CryptoProvider};
40pub use state::GroupState;
41
42#[cfg(feature = "by_ref_proposal")]
43use crate::crypto::{HpkePublicKey, HpkeSecretKey};
44
45use crate::extension::ExternalPubExt;
46
47use self::message_hash::MessageHash;
48#[cfg(feature = "private_message")]
49use self::mls_rules::{EncryptionOptions, MlsRules};
50
51#[cfg(feature = "psk")]
52pub use self::resumption::ReinitClient;
53
54#[cfg(feature = "psk")]
55use crate::psk::{
56    resolver::PskResolver, secret::PskSecretInput, ExternalPskId, JustPreSharedKeyID, PskGroupId,
57    ResumptionPSKUsage, ResumptionPsk,
58};
59
60#[cfg(feature = "private_message")]
61use ciphertext_processor::*;
62
63use component_operation::{ComponentID, ComponentOperationLabel};
64use confirmation_tag::*;
65use framing::*;
66use key_schedule::*;
67use membership_tag::*;
68use message_signature::*;
69use message_verifier::*;
70use proposal::*;
71#[cfg(feature = "by_ref_proposal")]
72use proposal_cache::*;
73use transcript_hash::*;
74
75#[cfg(test)]
76pub(crate) use self::commit::test_utils::CommitModifiers;
77
78#[cfg(all(test, feature = "private_message"))]
79pub use self::framing::PrivateMessage;
80
81use self::proposal_filter::ProposalInfo;
82
83#[cfg(any(feature = "secret_tree_access", feature = "private_message"))]
84use secret_tree::*;
85
86#[cfg(feature = "prior_epoch")]
87use self::epoch::PriorEpoch;
88
89use self::epoch::EpochSecrets;
90pub use self::message_processor::{
91    ApplicationMessageDescription, CommitEffect, CommitMessageDescription, NewEpoch,
92    ProposalMessageDescription, ProposalSender, ReceivedMessage,
93};
94use self::message_processor::{EventOrContent, MessageProcessor, ProvisionalState};
95#[cfg(feature = "by_ref_proposal")]
96use self::proposal_ref::ProposalRef;
97use self::state_repo::GroupStateRepository;
98pub use group_info::GroupInfo;
99
100pub use self::framing::{ContentType, Sender};
101pub use commit::*;
102pub use mls_rs_core::group::GroupContext;
103pub use roster::*;
104
105pub(crate) use mls_rs_core::group::ConfirmedTranscriptHash;
106pub(crate) use util::*;
107
108#[cfg(all(feature = "by_ref_proposal", feature = "external_client"))]
109pub use self::message_processor::CachedProposal;
110
111#[cfg(feature = "private_message")]
112mod ciphertext_processor;
113
114mod commit;
115pub mod component_operation;
116pub(crate) mod confirmation_tag;
117pub(crate) mod epoch;
118pub(crate) mod framing;
119mod group_info;
120pub(crate) mod key_schedule;
121mod membership_tag;
122pub(crate) mod message_hash;
123pub(crate) mod message_processor;
124pub(crate) mod message_signature;
125pub(crate) mod message_verifier;
126pub mod mls_rules;
127#[cfg(feature = "private_message")]
128pub(crate) mod padding;
129/// Proposals to evolve a MLS [`Group`]
130pub mod proposal;
131mod proposal_cache;
132pub(crate) mod proposal_filter;
133#[cfg(feature = "by_ref_proposal")]
134pub(crate) mod proposal_ref;
135#[cfg(feature = "psk")]
136mod resumption;
137mod roster;
138pub(crate) mod snapshot;
139pub(crate) mod state;
140
141#[cfg(feature = "prior_epoch")]
142pub(crate) mod state_repo;
143#[cfg(not(feature = "prior_epoch"))]
144pub(crate) mod state_repo_light;
145#[cfg(not(feature = "prior_epoch"))]
146pub(crate) use state_repo_light as state_repo;
147
148pub(crate) mod transcript_hash;
149mod util;
150
151/// External commit building.
152pub mod external_commit;
153
154#[cfg(any(feature = "secret_tree_access", feature = "private_message"))]
155pub(crate) mod secret_tree;
156
157#[cfg(any(feature = "secret_tree_access", feature = "private_message"))]
158pub use secret_tree::MessageKeyData as MessageKey;
159
160#[cfg(all(test, feature = "rfc_compliant"))]
161mod interop_test_vectors;
162
163mod exported_tree;
164
165pub use exported_tree::ExportedTree;
166
167#[derive(Clone, Debug, PartialEq, MlsSize, MlsEncode, MlsDecode)]
168struct GroupSecrets {
169    joiner_secret: JoinerSecret,
170    path_secret: Option<PathSecret>,
171    psks: Vec<PreSharedKeyID>,
172}
173
174impl HpkeEncryptable for GroupSecrets {
175    const ENCRYPT_LABEL: &'static str = "Welcome";
176
177    fn from_bytes(bytes: Vec<u8>) -> Result<Self, MlsError> {
178        Self::mls_decode(&mut bytes.as_slice()).map_err(Into::into)
179    }
180
181    fn get_bytes(&self) -> Result<Vec<u8>, MlsError> {
182        self.mls_encode_to_vec().map_err(Into::into)
183    }
184}
185
186#[derive(Clone, Debug, PartialEq, Eq, MlsSize, MlsEncode, MlsDecode)]
187#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
188pub(crate) struct EncryptedGroupSecrets {
189    pub new_member: KeyPackageRef,
190    pub encrypted_group_secrets: HpkeCiphertext,
191}
192
193#[derive(Clone, Eq, PartialEq, MlsSize, MlsEncode, MlsDecode)]
194#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
195pub(crate) struct Welcome {
196    pub cipher_suite: CipherSuite,
197    pub secrets: Vec<EncryptedGroupSecrets>,
198    #[mls_codec(with = "mls_rs_codec::byte_vec")]
199    pub encrypted_group_info: Vec<u8>,
200}
201
202impl Debug for Welcome {
203    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204        f.debug_struct("Welcome")
205            .field("cipher_suite", &self.cipher_suite)
206            .field("secrets", &self.secrets)
207            .field(
208                "encrypted_group_info",
209                &mls_rs_core::debug::pretty_bytes(&self.encrypted_group_info),
210            )
211            .finish()
212    }
213}
214
215#[derive(Clone, Debug)]
216#[cfg_attr(
217    all(feature = "ffi", not(test)),
218    safer_ffi_gen::ffi_type(clone, opaque)
219)]
220#[non_exhaustive]
221/// Information provided to new members upon joining a group.
222pub struct NewMemberInfo {
223    /// Group info extensions found within the Welcome message used to join
224    /// the group.
225    pub group_info_extensions: ExtensionList,
226    /// The group member who generated the Commit adding the joiner to the
227    /// group. This may not be the party who generated the corresponding
228    /// add proposal
229    pub sender: u32,
230}
231
232#[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen)]
233impl NewMemberInfo {
234    pub(crate) fn new(group_info_extensions: ExtensionList, sender: u32) -> Self {
235        let mut new_member_info = Self {
236            group_info_extensions,
237            sender,
238        };
239
240        new_member_info.ungrease();
241
242        new_member_info
243    }
244
245    /// Group info extensions found within the Welcome message used to join
246    /// the group.
247    #[cfg(feature = "ffi")]
248    pub fn group_info_extensions(&self) -> &ExtensionList {
249        &self.group_info_extensions
250    }
251}
252
253/// An MLS end-to-end encrypted group.
254///
255/// # Group Evolution
256///
257/// MLS Groups are evolved via a propose-then-commit system. Each group state
258/// produced by a commit is called an epoch and can produce and consume
259/// application, proposal, and commit messages. A [commit](Group::commit) is used
260/// to advance to the next epoch by applying existing proposals sent in
261/// the current epoch by-reference along with an optional set of proposals
262/// that are included by-value using a [`CommitBuilder`].
263#[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::ffi_type(opaque))]
264#[derive(Clone)]
265pub struct Group<C>
266where
267    C: ClientConfig,
268{
269    config: C,
270    cipher_suite_provider: <C::CryptoProvider as CryptoProvider>::CipherSuiteProvider,
271    state_repo: GroupStateRepository<C::GroupStateStorage, C::KeyPackageRepository>,
272    pub(crate) state: GroupState,
273    epoch_secrets: EpochSecrets,
274    private_tree: TreeKemPrivate,
275    key_schedule: KeySchedule,
276    #[cfg(feature = "by_ref_proposal")]
277    pending_updates:
278        crate::map::SmallMap<HpkePublicKey, (HpkeSecretKey, Option<SignatureSecretKey>)>,
279    pending_commit: PendingCommitSnapshot,
280    #[cfg(feature = "psk")]
281    previous_psk: Option<PskSecretInput>,
282    #[cfg(test)]
283    pub(crate) commit_modifiers: CommitModifiers,
284    pub(crate) signer: SignatureSecretKey,
285}
286
287#[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen)]
288impl<C> Group<C>
289where
290    C: ClientConfig + Clone,
291{
292    #[allow(clippy::too_many_arguments)]
293    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
294    pub(crate) async fn new(
295        config: C,
296        group_id: Option<Vec<u8>>,
297        cipher_suite: CipherSuite,
298        protocol_version: ProtocolVersion,
299        signing_identity: SigningIdentity,
300        group_context_extensions: ExtensionList,
301        leaf_node_extensions: ExtensionList,
302        signer: SignatureSecretKey,
303        maybe_now_time: Option<MlsTime>,
304    ) -> Result<Self, MlsError> {
305        let cipher_suite_provider = cipher_suite_provider(config.crypto_provider(), cipher_suite)?;
306
307        let (leaf_node, leaf_node_secret) = LeafNode::generate(
308            &cipher_suite_provider,
309            config.leaf_properties(leaf_node_extensions),
310            signing_identity,
311            &signer,
312            config.lifetime(maybe_now_time),
313        )
314        .await?;
315
316        let (mut public_tree, private_tree) = TreeKemPublic::derive(
317            leaf_node,
318            leaf_node_secret,
319            &config.identity_provider(),
320            &group_context_extensions,
321        )
322        .await?;
323
324        let tree_hash = public_tree.tree_hash(&cipher_suite_provider).await?;
325
326        let group_id = group_id.map(Ok).unwrap_or_else(|| {
327            cipher_suite_provider
328                .random_bytes_vec(cipher_suite_provider.kdf_extract_size())
329                .map_err(|e| MlsError::CryptoProviderError(e.into_any_error()))
330        })?;
331
332        let context = GroupContext::new(
333            protocol_version,
334            cipher_suite,
335            group_id,
336            tree_hash,
337            group_context_extensions,
338        );
339
340        let identity_provider = config.identity_provider();
341
342        let member_validation_context = MemberValidationContext::ForNewGroup {
343            current_context: &context,
344        };
345
346        let leaf_node_validator = LeafNodeValidator::new(
347            &cipher_suite_provider,
348            &identity_provider,
349            member_validation_context,
350        );
351
352        leaf_node_validator
353            .check_if_valid(
354                public_tree.get_leaf_node(LeafIndex::unchecked(0))?,
355                ValidationContext::Add(maybe_now_time),
356            )
357            .await?;
358
359        let state_repo = GroupStateRepository::new(
360            #[cfg(feature = "prior_epoch")]
361            context.group_id.clone(),
362            config.group_state_storage(),
363            config.key_package_repo(),
364            None,
365        )?;
366
367        let key_schedule_result = KeySchedule::from_random_epoch_secret(
368            &cipher_suite_provider,
369            #[cfg(any(feature = "secret_tree_access", feature = "private_message"))]
370            public_tree.total_leaf_count(),
371        )
372        .await?;
373
374        let confirmation_tag = ConfirmationTag::create(
375            &key_schedule_result.confirmation_key,
376            &vec![].into(),
377            &cipher_suite_provider,
378        )
379        .await?;
380
381        let interim_hash = InterimTranscriptHash::create(
382            &cipher_suite_provider,
383            &vec![].into(),
384            &confirmation_tag,
385        )
386        .await?;
387
388        Ok(Self {
389            config,
390            state: GroupState::new(context, public_tree, interim_hash, confirmation_tag),
391            private_tree,
392            key_schedule: key_schedule_result.key_schedule,
393            #[cfg(feature = "by_ref_proposal")]
394            pending_updates: Default::default(),
395            pending_commit: Default::default(),
396            #[cfg(test)]
397            commit_modifiers: Default::default(),
398            epoch_secrets: key_schedule_result.epoch_secrets,
399            state_repo,
400            cipher_suite_provider,
401            #[cfg(feature = "psk")]
402            previous_psk: None,
403            signer,
404        })
405    }
406
407    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
408    pub(crate) async fn join(
409        welcome: &MlsMessage,
410        tree_data: Option<ExportedTree<'_>>,
411        config: C,
412        signer: SignatureSecretKey,
413        maybe_time: Option<MlsTime>,
414    ) -> Result<(Self, NewMemberInfo), MlsError> {
415        Self::from_welcome_message(
416            welcome,
417            tree_data,
418            config,
419            signer,
420            #[cfg(feature = "psk")]
421            None,
422            maybe_time,
423        )
424        .await
425    }
426
427    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
428    async fn from_welcome_message(
429        welcome: &MlsMessage,
430        tree_data: Option<ExportedTree<'_>>,
431        config: C,
432        signer: SignatureSecretKey,
433        #[cfg(feature = "psk")] additional_psk: Option<PskSecretInput>,
434        maybe_time: Option<MlsTime>,
435    ) -> Result<(Self, NewMemberInfo), MlsError> {
436        let (group_info, key_package_generation, group_secrets, psk_secret) =
437            Self::decrypt_group_info_internal(
438                welcome,
439                &config,
440                #[cfg(feature = "psk")]
441                additional_psk,
442            )
443            .await?;
444
445        let cipher_suite_provider = cipher_suite_provider(
446            config.crypto_provider(),
447            group_info.group_context.cipher_suite,
448        )?;
449
450        let id_provider = config.identity_provider();
451
452        let public_tree = validate_tree_and_info_joiner(
453            welcome.version,
454            &group_info,
455            tree_data,
456            &id_provider,
457            &cipher_suite_provider,
458            maybe_time,
459        )
460        .await?;
461
462        let key_package = key_package_generation.key_package;
463
464        // Identify a leaf in the tree array (any even-numbered node) whose leaf_node is identical
465        // to the leaf_node field of the KeyPackage. If no such field exists, return an error. Let
466        // index represent the index of this node among the leaves in the tree, namely the index of
467        // the node in the tree array divided by two.
468        let self_index = public_tree
469            .find_leaf_node(&key_package.leaf_node)
470            .ok_or(MlsError::WelcomeKeyPackageNotFound)?;
471
472        #[cfg(not(feature = "last_resort_key_package_ext"))]
473        let is_last_resort = false;
474        #[cfg(feature = "last_resort_key_package_ext")]
475        let is_last_resort = key_package
476            .extensions
477            .has_extension(LastResortKeyPackageExt::extension_type());
478        // Delete the key just used if this is not a last-resort key package.
479        let used_key_package_ref = (!is_last_resort).then_some(key_package_generation.reference);
480
481        let mut private_tree =
482            TreeKemPrivate::new_self_leaf(self_index, key_package_generation.leaf_node_secret_key);
483
484        // If the path_secret value is set in the GroupSecrets object
485        if let Some(path_secret) = group_secrets.path_secret {
486            private_tree
487                .update_secrets(
488                    &cipher_suite_provider,
489                    group_info.signer,
490                    path_secret,
491                    &public_tree,
492                )
493                .await?;
494        }
495
496        // Use the joiner_secret from the GroupSecrets object to generate the epoch secret and
497        // other derived secrets for the current epoch.
498        let key_schedule_result = KeySchedule::from_joiner(
499            &cipher_suite_provider,
500            &group_secrets.joiner_secret,
501            &group_info.group_context,
502            #[cfg(any(feature = "secret_tree_access", feature = "private_message"))]
503            public_tree.total_leaf_count(),
504            &psk_secret,
505        )
506        .await?;
507
508        // Verify the confirmation tag in the GroupInfo using the derived confirmation key and the
509        // confirmed_transcript_hash from the GroupInfo.
510        if !group_info
511            .confirmation_tag
512            .matches(
513                &key_schedule_result.confirmation_key,
514                &group_info.group_context.confirmed_transcript_hash,
515                &cipher_suite_provider,
516            )
517            .await?
518        {
519            return Err(MlsError::InvalidConfirmationTag);
520        }
521
522        Self::join_with(
523            config,
524            group_info,
525            public_tree,
526            key_schedule_result.key_schedule,
527            key_schedule_result.epoch_secrets,
528            private_tree,
529            used_key_package_ref,
530            signer,
531        )
532        .await
533    }
534
535    #[allow(clippy::too_many_arguments)]
536    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
537    async fn join_with(
538        config: C,
539        group_info: GroupInfo,
540        public_tree: TreeKemPublic,
541        key_schedule: KeySchedule,
542        epoch_secrets: EpochSecrets,
543        private_tree: TreeKemPrivate,
544        used_key_package_ref: Option<KeyPackageRef>,
545        signer: SignatureSecretKey,
546    ) -> Result<(Self, NewMemberInfo), MlsError> {
547        let cs = group_info.group_context.cipher_suite;
548
549        let cs = config
550            .crypto_provider()
551            .cipher_suite_provider(cs)
552            .ok_or(MlsError::UnsupportedCipherSuite(cs))?;
553
554        // Use the confirmed transcript hash and confirmation tag to compute the interim transcript
555        // hash in the new state.
556        let interim_transcript_hash = InterimTranscriptHash::create(
557            &cs,
558            &group_info.group_context.confirmed_transcript_hash,
559            &group_info.confirmation_tag,
560        )
561        .await?;
562
563        let state_repo = GroupStateRepository::new(
564            #[cfg(feature = "prior_epoch")]
565            group_info.group_context.group_id.clone(),
566            config.group_state_storage(),
567            config.key_package_repo(),
568            used_key_package_ref,
569        )?;
570
571        let group = Group {
572            config,
573            state: GroupState::new(
574                group_info.group_context,
575                public_tree,
576                interim_transcript_hash,
577                group_info.confirmation_tag,
578            ),
579            private_tree,
580            key_schedule,
581            #[cfg(feature = "by_ref_proposal")]
582            pending_updates: Default::default(),
583            pending_commit: Default::default(),
584            #[cfg(test)]
585            commit_modifiers: Default::default(),
586            epoch_secrets,
587            state_repo,
588            cipher_suite_provider: cs,
589            #[cfg(feature = "psk")]
590            previous_psk: None,
591            signer,
592        };
593
594        Ok((
595            group,
596            NewMemberInfo::new(group_info.extensions, *group_info.signer),
597        ))
598    }
599
600    #[inline(always)]
601    pub(crate) fn current_epoch_tree(&self) -> &TreeKemPublic {
602        &self.state.public_tree
603    }
604
605    /// The current epoch of the group. This value is incremented each
606    /// time a [`Group::commit`] message is processed.
607    #[inline(always)]
608    pub fn current_epoch(&self) -> u64 {
609        self.context().epoch
610    }
611
612    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
613    async fn hpke_encrypt_to_recipient_with_generic_context(
614        &self,
615        recipient_index: u32,
616        context_info: &[u8],
617        associated_data: Option<&[u8]>,
618        plaintext: &[u8],
619    ) -> Result<HpkeCiphertext, MlsError> {
620        let member_leaf_node = self
621            .group_state()
622            .public_tree
623            .get_leaf_node(LeafIndex::try_from(recipient_index)?)?;
624        let member_public_key = &member_leaf_node.public_key;
625        let hpke_ciphertext = self
626            .cipher_suite_provider
627            .hpke_seal(member_public_key, context_info, associated_data, plaintext)
628            .await
629            .map_err(|e| MlsError::CryptoProviderError(e.into_any_error()))?;
630        Ok(hpke_ciphertext)
631    }
632
633    /// HPKE encrypts a message to the member at the specified `recipient_index` in the group.
634    ///
635    /// Takes `context_info`, `associated_data`, and `plaintext`.
636    /// Returns `ciphertext` and `kem_output` inside `HpkeCiphertext`.
637    ///
638    /// WARNING: The message sender is not authenticated.
639    #[cfg(feature = "non_domain_separated_hpke_encrypt_decrypt")]
640    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
641    #[cfg_attr(feature = "ffi", safer_ffi_gen::safer_ffi_gen_ignore)]
642    pub async fn hpke_encrypt_to_recipient(
643        &self,
644        recipient_index: u32,
645        context_info: &[u8],
646        associated_data: Option<&[u8]>,
647        plaintext: &[u8],
648    ) -> Result<HpkeCiphertext, MlsError> {
649        self.hpke_encrypt_to_recipient_with_generic_context(
650            recipient_index,
651            context_info,
652            associated_data,
653            plaintext,
654        )
655        .await
656    }
657
658    /// HPKE encrypts a message to the member at the specified `recipient_index` in the group.
659    ///
660    /// Takes a `component_id` and `context` to construct a `ComponentOperationLabel`, to be used as
661    /// the HPKE seal context, to ensure domain separation.
662    /// Also takes an `associated_data` and `plaintext`.
663    /// Returns `ciphertext` and `kem_output` inside `HpkeCiphertext`.
664    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
665    pub async fn safe_encrypt_with_context_to_recipient(
666        &self,
667        recipient_index: u32,
668        component_id: ComponentID,
669        context: &[u8],
670        associated_data: Option<&[u8]>,
671        plaintext: &[u8],
672    ) -> Result<HpkeCiphertext, MlsError> {
673        let component_operation_label = ComponentOperationLabel::new(component_id, context);
674        self.hpke_encrypt_to_recipient_with_generic_context(
675            recipient_index,
676            &component_operation_label.get_bytes()?,
677            associated_data,
678            plaintext,
679        )
680        .await
681    }
682
683    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
684    async fn hpke_decrypt_for_current_member_with_generic_context(
685        &self,
686        context_info: &[u8],
687        associated_data: Option<&[u8]>,
688        hpke_ciphertext: HpkeCiphertext,
689    ) -> Result<Vec<u8>, MlsError> {
690        let self_private_key = &self.private_tree.secret_keys[0]
691            .as_ref()
692            .ok_or(MlsError::InvalidTreeKemPrivateKey)?;
693        let self_public_key = &self.current_user_leaf_node()?.public_key;
694        let plaintext = self
695            .cipher_suite_provider
696            .hpke_open(
697                &hpke_ciphertext,
698                self_private_key,
699                self_public_key,
700                context_info,
701                associated_data,
702            )
703            .await
704            .map_err(|e| MlsError::CryptoProviderError(e.into_any_error()))?;
705        Ok(plaintext)
706    }
707
708    /// HPKE decrypts a message sent to the current member.
709    ///
710    /// Takes `HpkeCiphertext` generated by `hpke_encrypt_to_recipient` intended for the
711    /// current member.
712    ///
713    /// WARNING: The message sender is not authenticated.
714    #[cfg(feature = "non_domain_separated_hpke_encrypt_decrypt")]
715    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
716    #[cfg_attr(feature = "ffi", safer_ffi_gen::safer_ffi_gen_ignore)]
717    pub async fn hpke_decrypt_for_current_member(
718        &self,
719        context_info: &[u8],
720        associated_data: Option<&[u8]>,
721        hpke_ciphertext: HpkeCiphertext,
722    ) -> Result<Vec<u8>, MlsError> {
723        self.hpke_decrypt_for_current_member_with_generic_context(
724            context_info,
725            associated_data,
726            hpke_ciphertext,
727        )
728        .await
729    }
730
731    /// HPKE decrypts a message sent to the current member.
732    ///
733    /// Takes `HpkeCiphertext` generated by `hpke_encrypt_to_recipient` intended for the
734    /// current member.
735    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
736    pub async fn safe_decrypt_with_context_for_current_member(
737        &self,
738        component_id: ComponentID,
739        context: &[u8],
740        associated_data: Option<&[u8]>,
741        hpke_ciphertext: HpkeCiphertext,
742    ) -> Result<Vec<u8>, MlsError> {
743        let component_operation_label = ComponentOperationLabel::new(component_id, context);
744        self.hpke_decrypt_for_current_member_with_generic_context(
745            &component_operation_label.get_bytes()?,
746            associated_data,
747            hpke_ciphertext,
748        )
749        .await
750    }
751
752    /// Index within the group's state for the local group instance.
753    ///
754    /// This index corresponds to indexes in content descriptions within
755    /// [`ReceivedMessage`].
756    #[inline(always)]
757    pub fn current_member_index(&self) -> u32 {
758        *self.current_member_leaf_index()
759    }
760
761    #[inline(always)]
762    fn current_member_leaf_index(&self) -> LeafIndex {
763        self.private_tree.self_index
764    }
765
766    fn current_user_leaf_node(&self) -> Result<&LeafNode, MlsError> {
767        self.current_epoch_tree()
768            .get_leaf_node(self.private_tree.self_index)
769    }
770
771    /// Signing identity currently in use by the local group instance.
772    #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen_ignore)]
773    pub fn current_member_signing_identity(&self) -> Result<&SigningIdentity, MlsError> {
774        self.current_user_leaf_node().map(|ln| &ln.signing_identity)
775    }
776
777    /// Member at a specific index in the group state.
778    ///
779    /// These indexes correspond to indexes in content descriptions within
780    /// [`ReceivedMessage`].
781    pub fn member_at_index(&self, index: u32) -> Option<Member> {
782        self.group_state().member_at_index(index)
783    }
784
785    #[cfg(feature = "by_ref_proposal")]
786    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
787    async fn proposal_message(
788        &mut self,
789        proposal: Proposal,
790        authenticated_data: Vec<u8>,
791    ) -> Result<MlsMessage, MlsError> {
792        let sender = Sender::Member(*self.private_tree.self_index);
793
794        let auth_content = AuthenticatedContent::new_signed(
795            &self.cipher_suite_provider,
796            self.context(),
797            sender,
798            Content::Proposal(alloc::boxed::Box::new(proposal.clone())),
799            &self.signer,
800            #[cfg(feature = "private_message")]
801            self.encryption_options()?.control_wire_format(sender),
802            #[cfg(not(feature = "private_message"))]
803            WireFormat::PublicMessage,
804            authenticated_data,
805        )
806        .await?;
807
808        let sender = auth_content.content.sender;
809
810        let proposal_desc =
811            ProposalMessageDescription::new(&self.cipher_suite_provider, &auth_content, proposal)
812                .await?;
813
814        let message = self.format_for_wire(auth_content).await?;
815
816        self.state
817            .proposals
818            .insert_own(proposal_desc, &message, sender, &self.cipher_suite_provider)
819            .await?;
820
821        Ok(message)
822    }
823
824    /// Unique identifier for this group.
825    pub fn group_id(&self) -> &[u8] {
826        &self.context().group_id
827    }
828
829    fn provisional_private_tree(
830        &self,
831        provisional_state: &ProvisionalState,
832    ) -> Result<(TreeKemPrivate, Option<SignatureSecretKey>), MlsError> {
833        let mut provisional_private_tree = self.private_tree.clone();
834        let self_index = provisional_private_tree.self_index;
835
836        // Remove secret keys for blanked nodes
837        let path = provisional_state
838            .public_tree
839            .nodes
840            .direct_copath(self_index);
841
842        provisional_private_tree
843            .secret_keys
844            .resize(path.len() + 1, None);
845
846        for (i, n) in path.iter().enumerate() {
847            if provisional_state.public_tree.nodes.is_blank(n.path)? {
848                provisional_private_tree.secret_keys[i + 1] = None;
849            }
850        }
851
852        // Apply own update
853        let new_signer = None;
854
855        #[cfg(feature = "by_ref_proposal")]
856        let mut new_signer = new_signer;
857
858        #[cfg(feature = "by_ref_proposal")]
859        for p in &provisional_state.applied_proposals.updates {
860            if p.sender == Sender::Member(*self_index) {
861                let leaf_pk = &p.proposal.leaf_node.public_key;
862
863                // Update the leaf in the private tree if this is our update
864                #[cfg(feature = "std")]
865                let new_leaf_sk_and_signer = self.pending_updates.get(leaf_pk);
866
867                #[cfg(not(feature = "std"))]
868                let new_leaf_sk_and_signer = self
869                    .pending_updates
870                    .iter()
871                    .find_map(|(pk, sk)| (pk == leaf_pk).then_some(sk));
872
873                let new_leaf_sk = new_leaf_sk_and_signer.map(|(sk, _)| sk.clone());
874                new_signer = new_leaf_sk_and_signer.and_then(|(_, sk)| sk.clone());
875
876                provisional_private_tree
877                    .update_leaf(new_leaf_sk.ok_or(MlsError::UpdateErrorNoSecretKey)?);
878
879                break;
880            }
881        }
882
883        Ok((provisional_private_tree, new_signer))
884    }
885
886    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
887    async fn encrypt_group_secrets(
888        &self,
889        key_package: &KeyPackage,
890        leaf_index: LeafIndex,
891        joiner_secret: &JoinerSecret,
892        path_secrets: Option<&Vec<Option<PathSecret>>>,
893        #[cfg(feature = "psk")] psks: Vec<PreSharedKeyID>,
894        encrypted_group_info: &[u8],
895    ) -> Result<EncryptedGroupSecrets, MlsError> {
896        let path_secret = path_secrets
897            .map(|secrets| {
898                secrets
899                    .get(
900                        tree_math::leaf_lca_level(*self.private_tree.self_index, *leaf_index)
901                            as usize
902                            - 1,
903                    )
904                    .cloned()
905                    .flatten()
906                    .ok_or(MlsError::InvalidTreeKemPrivateKey)
907            })
908            .transpose()?;
909
910        #[cfg(not(feature = "psk"))]
911        let psks = Vec::new();
912
913        let group_secrets = GroupSecrets {
914            joiner_secret: joiner_secret.clone(),
915            path_secret,
916            psks,
917        };
918
919        let encrypted_group_secrets = group_secrets
920            .encrypt(
921                &self.cipher_suite_provider,
922                &key_package.hpke_init_key,
923                encrypted_group_info,
924            )
925            .await?;
926
927        Ok(EncryptedGroupSecrets {
928            new_member: key_package
929                .to_reference(&self.cipher_suite_provider)
930                .await?,
931            encrypted_group_secrets,
932        })
933    }
934
935    /// Create a proposal message that adds a new member to the group.
936    ///
937    /// `authenticated_data` will be sent unencrypted along with the contents
938    /// of the proposal message.
939    #[cfg(feature = "by_ref_proposal")]
940    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
941    pub async fn propose_add(
942        &mut self,
943        key_package: MlsMessage,
944        authenticated_data: Vec<u8>,
945    ) -> Result<MlsMessage, MlsError> {
946        let proposal = self.add_proposal(key_package)?;
947        self.proposal_message(proposal, authenticated_data).await
948    }
949
950    fn add_proposal(&self, key_package: MlsMessage) -> Result<Proposal, MlsError> {
951        Ok(Proposal::Add(alloc::boxed::Box::new(AddProposal {
952            key_package: key_package
953                .into_key_package()
954                .ok_or(MlsError::UnexpectedMessageType)?,
955        })))
956    }
957
958    /// Create a proposal message that updates your own public keys.
959    ///
960    /// This proposal is useful for contributing additional forward secrecy
961    /// and post-compromise security to the group without having to perform
962    /// the necessary computation of a [`Group::commit`].
963    ///
964    /// `authenticated_data` will be sent unencrypted along with the contents
965    /// of the proposal message.
966    #[cfg(feature = "by_ref_proposal")]
967    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
968    pub async fn propose_update(
969        &mut self,
970        authenticated_data: Vec<u8>,
971    ) -> Result<MlsMessage, MlsError> {
972        let proposal = self.update_proposal(None, None, None).await?;
973        self.proposal_message(proposal, authenticated_data).await
974    }
975
976    /// Create a proposal message that updates your own public keys
977    /// as well as your credential.
978    ///
979    /// This proposal is useful for contributing additional forward secrecy
980    /// and post-compromise security to the group without having to perform
981    /// the necessary computation of a [`Group::commit`].
982    ///
983    /// Identity updates are allowed by the group by default assuming that the
984    /// new identity provided is considered
985    /// [valid](crate::IdentityProvider::validate_member)
986    /// by and matches the output of the
987    /// [identity](crate::IdentityProvider)
988    /// function of the current
989    /// [`IdentityProvider`](crate::IdentityProvider).
990    ///
991    /// `authenticated_data` will be sent unencrypted along with the contents
992    /// of the proposal message.
993    #[cfg(feature = "by_ref_proposal")]
994    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
995    pub async fn propose_update_with_identity(
996        &mut self,
997        signer: SignatureSecretKey,
998        signing_identity: SigningIdentity,
999        authenticated_data: Vec<u8>,
1000    ) -> Result<MlsMessage, MlsError> {
1001        let proposal = self
1002            .update_proposal(Some(signer), Some(signing_identity), None)
1003            .await?;
1004
1005        self.proposal_message(proposal, authenticated_data).await
1006    }
1007
1008    #[cfg(feature = "by_ref_proposal")]
1009    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1010    async fn update_proposal(
1011        &mut self,
1012        signer: Option<SignatureSecretKey>,
1013        signing_identity: Option<SigningIdentity>,
1014        leaf_node_extensions: Option<ExtensionList>,
1015    ) -> Result<Proposal, MlsError> {
1016        // Grab a copy of the current node and update it to have new key material
1017        let mut new_leaf_node: LeafNode = self.current_user_leaf_node()?.clone();
1018
1019        let new_leaf_node_extensions =
1020            leaf_node_extensions.unwrap_or(new_leaf_node.ungreased_extensions());
1021        let secret_key = new_leaf_node
1022            .update(
1023                &self.cipher_suite_provider,
1024                self.group_id(),
1025                self.current_member_index(),
1026                Some(self.config.leaf_properties(new_leaf_node_extensions)),
1027                signing_identity,
1028                signer.as_ref().unwrap_or(&self.signer),
1029            )
1030            .await?;
1031
1032        // Store the secret key in the pending updates storage for later
1033        #[cfg(feature = "std")]
1034        self.pending_updates
1035            .insert(new_leaf_node.public_key.clone(), (secret_key, signer));
1036
1037        #[cfg(not(feature = "std"))]
1038        self.pending_updates
1039            .push((new_leaf_node.public_key.clone(), (secret_key, signer)));
1040
1041        Ok(Proposal::Update(UpdateProposal {
1042            leaf_node: new_leaf_node,
1043        }))
1044    }
1045
1046    /// Create a proposal message that removes an existing member from the
1047    /// group.
1048    ///
1049    /// `authenticated_data` will be sent unencrypted along with the contents
1050    /// of the proposal message.
1051    #[cfg(feature = "by_ref_proposal")]
1052    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1053    pub async fn propose_remove(
1054        &mut self,
1055        index: u32,
1056        authenticated_data: Vec<u8>,
1057    ) -> Result<MlsMessage, MlsError> {
1058        let proposal = self.remove_proposal(index)?;
1059        self.proposal_message(proposal, authenticated_data).await
1060    }
1061
1062    fn remove_proposal(&self, index: u32) -> Result<Proposal, MlsError> {
1063        let leaf_index = LeafIndex::try_from(index)?;
1064
1065        // Verify that this leaf is actually in the tree
1066        self.current_epoch_tree().get_leaf_node(leaf_index)?;
1067
1068        Ok(Proposal::Remove(RemoveProposal {
1069            to_remove: leaf_index,
1070        }))
1071    }
1072
1073    #[cfg(all(
1074        feature = "by_ref_proposal",
1075        feature = "custom_proposal",
1076        feature = "self_remove_proposal"
1077    ))]
1078    #[cfg_attr(feature = "ffi", safer_ffi_gen::safer_ffi_gen_ignore)]
1079    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1080    pub async fn propose_self_remove(
1081        &mut self,
1082        authenticated_data: Vec<u8>,
1083    ) -> Result<MlsMessage, MlsError> {
1084        if self.state.proposals.has_own_self_remove() {
1085            return Err(MlsError::SelfRemoveAlreadyProposed);
1086        }
1087        let proposal = Proposal::SelfRemove(SelfRemoveProposal {});
1088        self.proposal_message(proposal, authenticated_data).await
1089    }
1090
1091    /// Create a proposal message that adds an external pre shared key to the group.
1092    ///
1093    /// Each group member will need to have the PSK associated with
1094    /// [`ExternalPskId`](mls_rs_core::psk::ExternalPskId) installed within
1095    /// the [`PreSharedKeyStorage`](mls_rs_core::psk::PreSharedKeyStorage)
1096    /// in use by this group upon processing a [commit](Group::commit) that
1097    /// contains this proposal.
1098    ///
1099    /// `authenticated_data` will be sent unencrypted along with the contents
1100    /// of the proposal message.
1101    #[cfg(all(feature = "by_ref_proposal", feature = "psk"))]
1102    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1103    pub async fn propose_external_psk(
1104        &mut self,
1105        psk: ExternalPskId,
1106        authenticated_data: Vec<u8>,
1107    ) -> Result<MlsMessage, MlsError> {
1108        let proposal = self.psk_proposal(JustPreSharedKeyID::External(psk))?;
1109        self.proposal_message(proposal, authenticated_data).await
1110    }
1111
1112    #[cfg(feature = "psk")]
1113    fn psk_proposal(&self, key_id: JustPreSharedKeyID) -> Result<Proposal, MlsError> {
1114        Ok(Proposal::Psk(PreSharedKeyProposal {
1115            psk: PreSharedKeyID::new(key_id, &self.cipher_suite_provider)?,
1116        }))
1117    }
1118
1119    /// Create a proposal message that adds a pre shared key from a previous
1120    /// epoch to the current group state.
1121    ///
1122    /// Each group member will need to have the secret state from `psk_epoch`.
1123    /// In particular, the members who joined between `psk_epoch` and the
1124    /// current epoch cannot process a commit containing this proposal.
1125    ///
1126    /// `authenticated_data` will be sent unencrypted along with the contents
1127    /// of the proposal message.
1128    #[cfg(all(feature = "by_ref_proposal", feature = "psk"))]
1129    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1130    pub async fn propose_resumption_psk(
1131        &mut self,
1132        psk_epoch: u64,
1133        authenticated_data: Vec<u8>,
1134    ) -> Result<MlsMessage, MlsError> {
1135        let key_id = ResumptionPsk {
1136            psk_epoch,
1137            usage: ResumptionPSKUsage::Application,
1138            psk_group_id: PskGroupId(self.group_id().to_vec()),
1139        };
1140
1141        let proposal = self.psk_proposal(JustPreSharedKeyID::Resumption(key_id))?;
1142        self.proposal_message(proposal, authenticated_data).await
1143    }
1144
1145    /// Create a proposal message that requests for this group to be
1146    /// reinitialized.
1147    ///
1148    /// Once a [`ReInitProposal`](proposal::ReInitProposal)
1149    /// has been sent, another group member can complete reinitialization of
1150    /// the group by calling [`Group::get_reinit_client`].
1151    ///
1152    /// `authenticated_data` will be sent unencrypted along with the contents
1153    /// of the proposal message.
1154    #[cfg(feature = "by_ref_proposal")]
1155    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1156    pub async fn propose_reinit(
1157        &mut self,
1158        group_id: Option<Vec<u8>>,
1159        version: ProtocolVersion,
1160        cipher_suite: CipherSuite,
1161        extensions: ExtensionList,
1162        authenticated_data: Vec<u8>,
1163    ) -> Result<MlsMessage, MlsError> {
1164        let proposal = self.reinit_proposal(group_id, version, cipher_suite, extensions)?;
1165        self.proposal_message(proposal, authenticated_data).await
1166    }
1167
1168    fn reinit_proposal(
1169        &self,
1170        group_id: Option<Vec<u8>>,
1171        version: ProtocolVersion,
1172        cipher_suite: CipherSuite,
1173        extensions: ExtensionList,
1174    ) -> Result<Proposal, MlsError> {
1175        let group_id = group_id.map(Ok).unwrap_or_else(|| {
1176            self.cipher_suite_provider
1177                .random_bytes_vec(self.cipher_suite_provider.kdf_extract_size())
1178                .map_err(|e| MlsError::CryptoProviderError(e.into_any_error()))
1179        })?;
1180
1181        Ok(Proposal::ReInit(ReInitProposal {
1182            group_id,
1183            version,
1184            cipher_suite,
1185            extensions,
1186        }))
1187    }
1188
1189    /// Create a proposal message that sets extensions stored in the group
1190    /// state.
1191    ///
1192    /// # Warning
1193    ///
1194    /// This function does not create a diff that will be applied to the
1195    /// current set of extension that are in use. In order for an existing
1196    /// extension to not be overwritten by this proposal, it must be included
1197    /// in the new set of extensions being proposed.
1198    ///
1199    ///
1200    /// `authenticated_data` will be sent unencrypted along with the contents
1201    /// of the proposal message.
1202    #[cfg(feature = "by_ref_proposal")]
1203    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1204    pub async fn propose_group_context_extensions(
1205        &mut self,
1206        extensions: ExtensionList,
1207        authenticated_data: Vec<u8>,
1208    ) -> Result<MlsMessage, MlsError> {
1209        let proposal = self.group_context_extensions_proposal(extensions);
1210        self.proposal_message(proposal, authenticated_data).await
1211    }
1212
1213    fn group_context_extensions_proposal(&self, extensions: ExtensionList) -> Proposal {
1214        Proposal::GroupContextExtensions(extensions)
1215    }
1216
1217    /// Create a custom proposal message.
1218    ///
1219    /// `authenticated_data` will be sent unencrypted along with the contents
1220    /// of the proposal message.
1221    #[cfg(all(feature = "custom_proposal", feature = "by_ref_proposal"))]
1222    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1223    pub async fn propose_custom(
1224        &mut self,
1225        proposal: CustomProposal,
1226        authenticated_data: Vec<u8>,
1227    ) -> Result<MlsMessage, MlsError> {
1228        self.proposal_message(Proposal::Custom(proposal), authenticated_data)
1229            .await
1230    }
1231
1232    /// Validate a custom proposal message, verifying its signature and membership
1233    /// tag. Can be a proposal from the current or a prior epoch.
1234    /// Can also pass in a ProposalType to check the custom proposal against.
1235    /// Returns the `authenticated_data` and sender of the custom proposal, or an error if
1236    /// it fails to validate or if the message is not a custom proposal.
1237    ///
1238    /// WARNING: This API is not recommend for general purpose usage as it reduces the guarantees of RFC9420.
1239    /// Since prior epochs' membership keys are stored, an attacker that compromises a client's signature key
1240    /// and membership key is able to send valid MLS PublicMessages appearing to originate from the compromised
1241    /// client from prior epochs.
1242    /// This API is used by the GSMA's Rich Communication Suite for certain types of messages. See section 7.6
1243    /// for specifics https://www.gsma.com/solutions-and-impact/technologies/networks/wp-content/uploads/2025/03/RCC.16-v1.0.pdf.
1244    #[cfg(all(
1245        feature = "custom_proposal",
1246        feature = "by_ref_proposal",
1247        feature = "prior_epoch_membership_key"
1248    ))]
1249    #[cfg_attr(feature = "ffi", safer_ffi_gen::safer_ffi_gen_ignore)]
1250    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1251    pub async fn validate_custom_proposal(
1252        &mut self,
1253        msg: &MlsMessage,
1254        proposal_type: Option<ProposalType>,
1255    ) -> Result<(Vec<u8>, Sender), MlsError> {
1256        let auth_content = self.validate_public_message(msg).await?;
1257        let proposal = match auth_content.content.content {
1258            Content::Proposal(p) => match *p {
1259                Proposal::Custom(c) => c,
1260                _ => return Err(MlsError::UnexpectedMessageType),
1261            },
1262            _ => return Err(MlsError::UnexpectedMessageType),
1263        };
1264        if proposal_type.is_some() && Some(proposal.proposal_type()) != proposal_type {
1265            return Err(MlsError::UnsupportedCustomProposal(
1266                proposal.proposal_type(),
1267            ));
1268        }
1269        Ok((
1270            auth_content.content.authenticated_data,
1271            auth_content.content.sender,
1272        ))
1273    }
1274
1275    // Only used for custom proposals right now; consider removing this feature flag
1276    // if there are future use cases.
1277    #[cfg(all(feature = "custom_proposal", feature = "prior_epoch_membership_key"))]
1278    #[cfg_attr(feature = "ffi", safer_ffi_gen::safer_ffi_gen_ignore)]
1279    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1280    async fn validate_public_message(
1281        &mut self,
1282        msg: &MlsMessage,
1283    ) -> Result<AuthenticatedContent, MlsError> {
1284        let plaintext = match msg.payload {
1285            MlsMessagePayload::Plain(ref plaintext) => plaintext.clone(),
1286            _ => return Err(MlsError::UnexpectedMessageType),
1287        };
1288        let epoch_id = msg.epoch().ok_or(MlsError::EpochNotFound)?;
1289        let auth_content = if epoch_id == self.context().epoch {
1290            let auth_content = verify_plaintext_authentication(
1291                &self.cipher_suite_provider,
1292                plaintext,
1293                Some(&self.key_schedule.membership_key),
1294                &self.state.context,
1295                SignaturePublicKeysContainer::RatchetTree(&self.state.public_tree),
1296            )
1297            .await?;
1298
1299            Ok::<_, MlsError>(auth_content)
1300        } else {
1301            #[cfg(feature = "prior_epoch")]
1302            {
1303                let epoch = self
1304                    .state_repo
1305                    .get_epoch_mut(epoch_id)
1306                    .await?
1307                    .ok_or(MlsError::EpochNotFound)?;
1308
1309                let auth_content = verify_plaintext_authentication(
1310                    &self.cipher_suite_provider,
1311                    plaintext,
1312                    Some(&epoch.membership_key),
1313                    &epoch.context,
1314                    SignaturePublicKeysContainer::List(&epoch.signature_public_keys),
1315                )
1316                .await?;
1317
1318                Ok(auth_content)
1319            }
1320
1321            #[cfg(not(feature = "prior_epoch"))]
1322            Err(MlsError::EpochNotFound)
1323        }?;
1324
1325        Ok(auth_content)
1326    }
1327
1328    /// Delete all sent and received proposals cached for commit.
1329    #[cfg(feature = "by_ref_proposal")]
1330    pub fn clear_proposal_cache(&mut self) {
1331        self.state.proposals.clear()
1332    }
1333
1334    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1335    pub(crate) async fn format_for_wire(
1336        &mut self,
1337        content: AuthenticatedContent,
1338    ) -> Result<MlsMessage, MlsError> {
1339        #[cfg(feature = "private_message")]
1340        let payload = if content.wire_format == WireFormat::PrivateMessage {
1341            MlsMessagePayload::Cipher(self.create_ciphertext(content).await?)
1342        } else {
1343            MlsMessagePayload::Plain(self.create_plaintext(content).await?)
1344        };
1345        #[cfg(not(feature = "private_message"))]
1346        let payload = MlsMessagePayload::Plain(self.create_plaintext(content).await?);
1347
1348        Ok(MlsMessage::new(self.protocol_version(), payload))
1349    }
1350
1351    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1352    async fn create_plaintext(
1353        &self,
1354        auth_content: AuthenticatedContent,
1355    ) -> Result<PublicMessage, MlsError> {
1356        let membership_tag = if matches!(auth_content.content.sender, Sender::Member(_)) {
1357            let tag = self
1358                .key_schedule
1359                .get_membership_tag(&auth_content, self.context(), &self.cipher_suite_provider)
1360                .await?;
1361
1362            Some(tag)
1363        } else {
1364            None
1365        };
1366
1367        Ok(PublicMessage {
1368            content: auth_content.content,
1369            auth: auth_content.auth,
1370            membership_tag,
1371        })
1372    }
1373
1374    #[cfg(feature = "private_message")]
1375    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1376    async fn create_ciphertext(
1377        &mut self,
1378        auth_content: AuthenticatedContent,
1379    ) -> Result<PrivateMessage, MlsError> {
1380        let padding_mode = self.encryption_options()?.padding_mode;
1381
1382        let mut encryptor = CiphertextProcessor::new(self, self.cipher_suite_provider.clone());
1383
1384        encryptor.seal(auth_content, padding_mode).await
1385    }
1386
1387    /// Encrypt an application message using the current group state.
1388    ///
1389    /// `authenticated_data` will be sent unencrypted along with the contents
1390    /// of the proposal message.
1391    #[cfg(feature = "private_message")]
1392    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1393    pub async fn encrypt_application_message(
1394        &mut self,
1395        message: &[u8],
1396        authenticated_data: Vec<u8>,
1397    ) -> Result<MlsMessage, MlsError> {
1398        // A group member that has observed one or more proposals within an epoch MUST send a Commit message
1399        // before sending application data
1400        #[cfg(feature = "by_ref_proposal")]
1401        if !self.state.proposals.is_empty() {
1402            return Err(MlsError::CommitRequired);
1403        }
1404
1405        let auth_content = AuthenticatedContent::new_signed(
1406            &self.cipher_suite_provider,
1407            self.context(),
1408            Sender::Member(*self.private_tree.self_index),
1409            Content::Application(message.to_vec().into()),
1410            &self.signer,
1411            WireFormat::PrivateMessage,
1412            authenticated_data,
1413        )
1414        .await?;
1415
1416        self.format_for_wire(auth_content).await
1417    }
1418
1419    #[cfg(feature = "private_message")]
1420    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1421    async fn decrypt_incoming_ciphertext(
1422        &mut self,
1423        message: &PrivateMessage,
1424    ) -> Result<AuthenticatedContent, MlsError> {
1425        let epoch_id = message.epoch;
1426
1427        let auth_content = if epoch_id == self.context().epoch {
1428            let content = CiphertextProcessor::new(self, self.cipher_suite_provider.clone())
1429                .open(message)
1430                .await?;
1431
1432            verify_auth_content_signature(
1433                &self.cipher_suite_provider,
1434                SignaturePublicKeysContainer::RatchetTree(&self.state.public_tree),
1435                self.context(),
1436                &content,
1437                #[cfg(feature = "by_ref_proposal")]
1438                &[],
1439            )
1440            .await?;
1441
1442            Ok::<_, MlsError>(content)
1443        } else {
1444            #[cfg(feature = "prior_epoch")]
1445            {
1446                let epoch = self
1447                    .state_repo
1448                    .get_epoch_mut(epoch_id)
1449                    .await?
1450                    .ok_or(MlsError::EpochNotFound)?;
1451
1452                let content = CiphertextProcessor::new(epoch, self.cipher_suite_provider.clone())
1453                    .open(message)
1454                    .await?;
1455
1456                verify_auth_content_signature(
1457                    &self.cipher_suite_provider,
1458                    SignaturePublicKeysContainer::List(&epoch.signature_public_keys),
1459                    &epoch.context,
1460                    &content,
1461                    #[cfg(feature = "by_ref_proposal")]
1462                    &[],
1463                )
1464                .await?;
1465
1466                Ok(content)
1467            }
1468
1469            #[cfg(not(feature = "prior_epoch"))]
1470            Err(MlsError::EpochNotFound)
1471        }?;
1472
1473        Ok(auth_content)
1474    }
1475
1476    /// Apply a pending commit that was created by [`Group::commit`] or
1477    /// [`CommitBuilder::build`].
1478    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1479    pub async fn apply_pending_commit(&mut self) -> Result<CommitMessageDescription, MlsError> {
1480        let pending = core::mem::take(&mut self.pending_commit);
1481        self.apply_detached_commit(CommitSecrets(pending)).await
1482    }
1483
1484    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1485    pub async fn apply_pending_commit_backwards_compatible(
1486        &mut self,
1487    ) -> Result<CommitMessageDescription, MlsError> {
1488        let pending = core::mem::take(&mut self.pending_commit);
1489        self.apply_detached_commit_backwards_compatible(CommitSecrets(pending))
1490            .await
1491    }
1492
1493    /// Apply a detached commit that was created by [`Group::commit_detached`] or
1494    /// [`CommitBuilder::build_detached`].
1495    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1496    pub async fn apply_detached_commit(
1497        &mut self,
1498        commit_secrets: CommitSecrets,
1499    ) -> Result<CommitMessageDescription, MlsError> {
1500        let pending = match commit_secrets.0 {
1501            PendingCommitSnapshot::PendingCommit(bytes) => PendingCommit::mls_decode(&mut &*bytes)?,
1502            _ => return Err(MlsError::PendingCommitNotFound),
1503        };
1504
1505        self.insert_past_epoch().await?;
1506
1507        self.state = pending.state;
1508        self.epoch_secrets = pending.epoch_secrets;
1509        self.private_tree = pending.private_tree;
1510        self.key_schedule = pending.key_schedule;
1511        self.signer = pending.signer;
1512
1513        Ok(pending.output)
1514    }
1515
1516    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1517    pub async fn apply_detached_commit_backwards_compatible(
1518        &mut self,
1519        commit_secrets: CommitSecrets,
1520    ) -> Result<CommitMessageDescription, MlsError> {
1521        match commit_secrets.0 {
1522            PendingCommitSnapshot::None | PendingCommitSnapshot::PendingCommit(_) => {
1523                self.apply_detached_commit(commit_secrets).await
1524            }
1525            PendingCommitSnapshot::LegacyPendingCommit(legacy_pending) => {
1526                let content = legacy_pending.content.clone();
1527                self.pending_commit = PendingCommitSnapshot::LegacyPendingCommit(legacy_pending);
1528                self.process_commit(content, None).await
1529            }
1530        }
1531    }
1532
1533    /// Returns true if a commit has been created but not yet applied
1534    /// with [`Group::apply_pending_commit`] or cleared with [`Group::clear_pending_commit`]
1535    pub fn has_pending_commit(&self) -> bool {
1536        !self.pending_commit.is_none()
1537    }
1538
1539    /// Clear the currently pending commit.
1540    ///
1541    /// This function will automatically be called in the event that a
1542    /// commit message is processed using [`Group::process_incoming_message`]
1543    /// before [`Group::apply_pending_commit`] is called.
1544    pub fn clear_pending_commit(&mut self) {
1545        self.pending_commit = Default::default()
1546    }
1547
1548    /// Returns true if the client has received or issued a proposal
1549    /// that needs to be committed to with [`Group::commit`] before encrypting an
1550    /// application message.
1551    #[cfg(feature = "by_ref_proposal")]
1552    pub fn commit_required(&self) -> bool {
1553        !self.state.proposals.is_empty()
1554    }
1555
1556    /// Process an inbound message for this group.
1557    ///
1558    /// # Warning
1559    ///
1560    /// Changes to the group's state as a result of processing `message` will
1561    /// not be persisted by the
1562    /// [`GroupStateStorage`](crate::GroupStateStorage)
1563    /// in use by this group until [`Group::write_to_storage`] is called.
1564    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1565    #[inline(never)]
1566    pub async fn process_incoming_message(
1567        &mut self,
1568        message: MlsMessage,
1569    ) -> Result<ReceivedMessage, MlsError> {
1570        if let Some(pending) = self.pending_commit.commit_hash()? {
1571            let message_hash = MessageHash::compute(&self.cipher_suite_provider, &message).await?;
1572
1573            if message_hash == pending {
1574                let message_description = self.apply_pending_commit().await?;
1575
1576                return Ok(ReceivedMessage::Commit(message_description));
1577            }
1578        }
1579
1580        #[cfg(feature = "by_ref_proposal")]
1581        if message.wire_format() == WireFormat::PrivateMessage {
1582            let cached_own_proposal = self
1583                .state
1584                .proposals
1585                .get_own(&self.cipher_suite_provider, &message)
1586                .await?;
1587
1588            if let Some(cached) = cached_own_proposal {
1589                return Ok(ReceivedMessage::Proposal(cached));
1590            }
1591        }
1592
1593        MessageProcessor::process_incoming_message(
1594            self,
1595            message,
1596            #[cfg(feature = "by_ref_proposal")]
1597            true,
1598        )
1599        .await
1600    }
1601
1602    /// Process an inbound message for this group, providing additional context
1603    /// with a message timestamp.
1604    ///
1605    /// Providing a timestamp is useful when the
1606    /// [`IdentityProvider`](crate::IdentityProvider)
1607    /// in use by the group can determine validity based on a timestamp.
1608    /// For example, this allows for checking X.509 certificate expiration
1609    /// at the time when `message` was received by a server rather than when
1610    /// a specific client asynchronously received `message`
1611    ///
1612    /// # Warning
1613    ///
1614    /// Changes to the group's state as a result of processing `message` will
1615    /// not be persisted by the
1616    /// [`GroupStateStorage`](crate::GroupStateStorage)
1617    /// in use by this group until [`Group::write_to_storage`] is called.
1618    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1619    pub async fn process_incoming_message_with_time(
1620        &mut self,
1621        message: MlsMessage,
1622        time: MlsTime,
1623    ) -> Result<ReceivedMessage, MlsError> {
1624        if let Some(pending) = self.pending_commit.commit_hash()? {
1625            let message_hash = MessageHash::compute(&self.cipher_suite_provider, &message).await?;
1626
1627            if message_hash == pending {
1628                let message_description = self.apply_pending_commit().await?;
1629
1630                return Ok(ReceivedMessage::Commit(message_description));
1631            }
1632        }
1633
1634        #[cfg(feature = "by_ref_proposal")]
1635        if message.wire_format() == WireFormat::PrivateMessage {
1636            let cached_own_proposal = self
1637                .state
1638                .proposals
1639                .get_own(&self.cipher_suite_provider, &message)
1640                .await?;
1641
1642            if let Some(cached) = cached_own_proposal {
1643                return Ok(ReceivedMessage::Proposal(cached));
1644            }
1645        }
1646
1647        MessageProcessor::process_incoming_message_with_time(
1648            self,
1649            message,
1650            #[cfg(feature = "by_ref_proposal")]
1651            true,
1652            Some(time),
1653        )
1654        .await
1655    }
1656
1657    /// Find a group member by
1658    /// [identity](crate::IdentityProvider::identity)
1659    ///
1660    /// This function determines identity by calling the
1661    /// [`IdentityProvider`](crate::IdentityProvider)
1662    /// currently in use by the group.
1663    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1664    pub async fn member_with_identity(&self, identity: &[u8]) -> Result<Member, MlsError> {
1665        let tree = &self.state.public_tree;
1666
1667        #[cfg(feature = "tree_index")]
1668        let index = tree.get_leaf_node_with_identity(identity);
1669
1670        #[cfg(not(feature = "tree_index"))]
1671        let index = tree
1672            .get_leaf_node_with_identity(
1673                identity,
1674                &self.identity_provider(),
1675                &self.state.context.extensions,
1676            )
1677            .await?;
1678
1679        let index = index.ok_or(MlsError::MemberNotFound)?;
1680        let node = self.state.public_tree.get_leaf_node(index)?;
1681
1682        Ok(member_from_leaf_node(node, index))
1683    }
1684
1685    /// Create a group info message that can be used for external proposals and commits.
1686    ///
1687    /// The returned `GroupInfo` is suitable for one external commit for the current epoch.
1688    /// If `with_tree_in_extension` is set to true, the returned `GroupInfo` contains the
1689    /// ratchet tree and therefore contains all information needed to join the group. Otherwise,
1690    /// the ratchet tree must be obtained separately, e.g. via
1691    /// (ExternalClient::export_tree)[crate::external_client::ExternalGroup::export_tree].
1692    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1693    pub async fn group_info_message_allowing_ext_commit(
1694        &self,
1695        with_tree_in_extension: bool,
1696    ) -> Result<MlsMessage, MlsError> {
1697        let mut extensions = ExtensionList::new();
1698
1699        extensions.set_from({
1700            self.key_schedule
1701                .get_external_key_pair_ext(&self.cipher_suite_provider)
1702                .await?
1703        })?;
1704
1705        self.group_info_message_internal(extensions, with_tree_in_extension)
1706            .await
1707    }
1708
1709    /// Create a group info message that can be used for external proposals.
1710    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1711    pub async fn group_info_message(
1712        &self,
1713        with_tree_in_extension: bool,
1714    ) -> Result<MlsMessage, MlsError> {
1715        self.group_info_message_internal(ExtensionList::new(), with_tree_in_extension)
1716            .await
1717    }
1718
1719    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1720    pub async fn group_info_message_internal(
1721        &self,
1722        mut initial_extensions: ExtensionList,
1723        with_tree_in_extension: bool,
1724    ) -> Result<MlsMessage, MlsError> {
1725        if with_tree_in_extension {
1726            initial_extensions.set_from(RatchetTreeExt {
1727                tree_data: ExportedTree::new(self.state.public_tree.nodes.clone()),
1728            })?;
1729        }
1730
1731        let mut info = GroupInfo {
1732            group_context: self.context().clone(),
1733            extensions: initial_extensions,
1734            confirmation_tag: self.state.confirmation_tag.clone(),
1735            signer: self.private_tree.self_index,
1736            signature: Vec::new(),
1737        };
1738
1739        info.grease(self.cipher_suite_provider())?;
1740
1741        info.sign(&self.cipher_suite_provider, &self.signer, &())
1742            .await?;
1743
1744        Ok(MlsMessage::new(
1745            self.protocol_version(),
1746            MlsMessagePayload::GroupInfo(info),
1747        ))
1748    }
1749
1750    /// Get the current group context summarizing various information about the group.
1751    #[inline(always)]
1752    pub fn context(&self) -> &GroupContext {
1753        &self.group_state().context
1754    }
1755
1756    /// Get the
1757    /// [epoch_authenticator](https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol.html#name-key-schedule)
1758    /// of the current epoch.
1759    pub fn epoch_authenticator(&self) -> Result<Secret, MlsError> {
1760        Ok(self.key_schedule.authentication_secret.clone().into())
1761    }
1762
1763    /// Export a secret for use outside of MLS. Each epoch, label, context
1764    /// combination has a unique and independent secret. Secrets for all
1765    /// epochs, labels and contexts can be derived until either the epoch
1766    /// changes, i.e. a commit is received (or own commit is applied), or
1767    /// [Group::delete_exporter] is called.
1768    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1769    pub async fn export_secret(
1770        &self,
1771        label: &[u8],
1772        context: &[u8],
1773        len: usize,
1774    ) -> Result<Secret, MlsError> {
1775        self.key_schedule
1776            .export_secret(label, context, len, &self.cipher_suite_provider)
1777            .await
1778            .map(Into::into)
1779    }
1780
1781    /// Delete the exporter secret. Afterwards the state contains no information
1782    /// about any secrets outputted by [Group::export_secret] (for the current or
1783    /// past epochs). This means that after calling this function, [Group::export_secret]
1784    /// can no longer be used and we get forward secrecy for all secrets derived using
1785    /// [Group::export_secret].
1786    pub fn delete_exporter(&mut self) {
1787        self.key_schedule.delete_exporter();
1788    }
1789
1790    /// Export the current epoch's ratchet tree in serialized format.
1791    ///
1792    /// This function is used to provide the current group tree to new members
1793    /// when the `ratchet_tree_extension` is not used according to [`MlsRules::commit_options`].
1794    pub fn export_tree(&self) -> ExportedTree<'_> {
1795        ExportedTree::new_borrowed(&self.current_epoch_tree().nodes)
1796    }
1797
1798    /// Current version of the MLS protocol in use by this group.
1799    pub fn protocol_version(&self) -> ProtocolVersion {
1800        self.context().protocol_version
1801    }
1802
1803    /// Current cipher suite in use by this group.
1804    pub fn cipher_suite(&self) -> CipherSuite {
1805        self.context().cipher_suite
1806    }
1807
1808    /// Current roster
1809    pub fn roster(&self) -> Roster<'_> {
1810        self.group_state().public_tree.roster()
1811    }
1812
1813    /// Determines equality of two different groups internal states.
1814    /// Useful for testing.
1815    ///
1816    pub fn equal_group_state(a: &Group<C>, b: &Group<C>) -> bool {
1817        a.state == b.state && a.key_schedule == b.key_schedule && a.epoch_secrets == b.epoch_secrets
1818    }
1819
1820    #[cfg(feature = "psk")]
1821    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1822    async fn get_psk(
1823        &self,
1824        psks: &[ProposalInfo<PreSharedKeyProposal>],
1825    ) -> Result<(PskSecret, Vec<PreSharedKeyID>), MlsError> {
1826        if let Some(psk) = self.previous_psk.clone() {
1827            // TODO consider throwing error if psks not empty
1828            let psk_id = vec![psk.id.clone()];
1829            let psk = PskSecret::calculate(&[psk], self.cipher_suite_provider()).await?;
1830
1831            Ok((psk, psk_id))
1832        } else {
1833            let psks = psks
1834                .iter()
1835                .map(|psk| psk.proposal.psk.clone())
1836                .collect::<Vec<_>>();
1837
1838            let psk = PskResolver {
1839                group_context: Some(self.context()),
1840                current_epoch: Some(&self.epoch_secrets),
1841                prior_epochs: Some(&self.state_repo),
1842                psk_store: &self.config.secret_store(),
1843            }
1844            .resolve_to_secret(&psks, self.cipher_suite_provider())
1845            .await?;
1846
1847            Ok((psk, psks))
1848        }
1849    }
1850
1851    #[cfg(feature = "private_message")]
1852    pub(crate) fn encryption_options(&self) -> Result<EncryptionOptions, MlsError> {
1853        self.config
1854            .mls_rules()
1855            .encryption_options(&self.roster(), self.group_context())
1856            .map_err(|e| MlsError::MlsRulesError(e.into_any_error()))
1857    }
1858
1859    #[cfg(not(feature = "psk"))]
1860    fn get_psk(&self) -> PskSecret {
1861        PskSecret::new(self.cipher_suite_provider())
1862    }
1863
1864    /// Returns the key generation used by the next invocation of
1865    /// [`Group::encrypt_application_message`]. Does not increment the generation nor
1866    /// derive keys.
1867    ///
1868    /// Used by clients to authenticate the generation to defend against in-group forgery
1869    /// attacks described in https://eprint.iacr.org/2025/554. This may be accomplished
1870    /// by placing the generation in the `message` or `authenticated_data` parameters of
1871    /// [`Group::encrypt_application_message`], as both fields are signed by the sender's
1872    /// signature key.
1873    ///
1874    /// To verify, get the unauthenticated generation from ApplicationMessageDescription
1875    /// returned from [`Group::process_incoming_message`], which is the value used to
1876    /// derive keys to decrypt the message, and check that it equals the authenticated
1877    /// generation.
1878    ///
1879    /// WARNING: This is only safe for synchronous usage of [`Group`] APIs.
1880    #[cfg(all(
1881        feature = "export_key_generation",
1882        feature = "private_message",
1883        feature = "secret_tree_access",
1884    ))]
1885    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1886    pub fn peek_next_key_generation(&mut self) -> Result<u32, MlsError> {
1887        self.epoch_secrets
1888            .secret_tree
1889            .peek_next_key_generation(
1890                &self.cipher_suite_provider,
1891                crate::tree_kem::node::NodeIndex::from(self.private_tree.self_index),
1892                KeyType::Application,
1893            )
1894            .await
1895    }
1896
1897    #[cfg(feature = "secret_tree_access")]
1898    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1899    #[inline(never)]
1900    pub async fn next_encryption_key(&mut self) -> Result<MessageKey, MlsError> {
1901        self.epoch_secrets
1902            .secret_tree
1903            .next_message_key(
1904                &self.cipher_suite_provider,
1905                crate::tree_kem::node::NodeIndex::from(self.private_tree.self_index),
1906                KeyType::Application,
1907            )
1908            .await
1909    }
1910
1911    #[cfg(feature = "secret_tree_access")]
1912    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1913    pub async fn derive_decryption_key(
1914        &mut self,
1915        sender: u32,
1916        generation: u32,
1917    ) -> Result<MessageKey, MlsError> {
1918        self.epoch_secrets
1919            .secret_tree
1920            .message_key_generation(
1921                &self.cipher_suite_provider,
1922                crate::tree_kem::node::NodeIndex::from(sender),
1923                KeyType::Application,
1924                generation,
1925            )
1926            .await
1927    }
1928}
1929
1930impl<C: ClientConfig> Group<C> {
1931    #[cfg(feature = "psk")]
1932    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1933    async fn psk_secret<CS: CipherSuiteProvider>(
1934        config: &C,
1935        cipher_suite_provider: &CS,
1936        psks: &[PreSharedKeyID],
1937        additional_psk: Option<PskSecretInput>,
1938    ) -> Result<PskSecret, MlsError> {
1939        if let Some(psk) = additional_psk {
1940            let psk_id = psks.first().ok_or(MlsError::UnexpectedPskId)?;
1941
1942            match &psk_id.key_id {
1943                JustPreSharedKeyID::Resumption(r) if r.usage != ResumptionPSKUsage::Application => {
1944                    Ok(())
1945                }
1946                _ => Err(MlsError::UnexpectedPskId),
1947            }?;
1948
1949            let mut psk = psk;
1950            psk.id.psk_nonce = psk_id.psk_nonce.clone();
1951            PskSecret::calculate(&[psk], cipher_suite_provider).await
1952        } else {
1953            PskResolver::<
1954                <C as ClientConfig>::GroupStateStorage,
1955                <C as ClientConfig>::KeyPackageRepository,
1956                <C as ClientConfig>::PskStore,
1957            > {
1958                group_context: None,
1959                current_epoch: None,
1960                prior_epochs: None,
1961                psk_store: &config.secret_store(),
1962            }
1963            .resolve_to_secret(psks, cipher_suite_provider)
1964            .await
1965        }
1966    }
1967
1968    #[cfg(not(feature = "psk"))]
1969    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1970    async fn psk_secret<CS: CipherSuiteProvider>(
1971        _config: &C,
1972        cipher_suite_provider: &CS,
1973        _psks: &[PreSharedKeyID],
1974    ) -> Result<PskSecret, MlsError> {
1975        Ok(PskSecret::new(cipher_suite_provider))
1976    }
1977
1978    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1979    pub(crate) async fn decrypt_group_info(
1980        welcome: &MlsMessage,
1981        config: &C,
1982    ) -> Result<GroupInfo, MlsError> {
1983        Self::decrypt_group_info_internal(
1984            welcome,
1985            config,
1986            #[cfg(feature = "psk")]
1987            None,
1988        )
1989        .await
1990        .map(|info| info.0)
1991    }
1992
1993    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1994    async fn decrypt_group_info_internal(
1995        welcome: &MlsMessage,
1996        config: &C,
1997        #[cfg(feature = "psk")] additional_psk: Option<PskSecretInput>,
1998    ) -> Result<(GroupInfo, KeyPackageGeneration, GroupSecrets, PskSecret), MlsError> {
1999        let protocol_version = welcome.version;
2000
2001        if !config.version_supported(protocol_version) {
2002            return Err(MlsError::UnsupportedProtocolVersion(protocol_version));
2003        }
2004
2005        let MlsMessagePayload::Welcome(welcome) = &welcome.payload else {
2006            return Err(MlsError::UnexpectedMessageType);
2007        };
2008
2009        let cipher_suite_provider =
2010            cipher_suite_provider(config.crypto_provider(), welcome.cipher_suite)?;
2011
2012        let (encrypted_group_secrets, key_package_generation) =
2013            find_key_package_generation(&config.key_package_repo(), &welcome.secrets).await?;
2014
2015        let key_package_version = key_package_generation.key_package.version;
2016
2017        if key_package_version != protocol_version {
2018            return Err(MlsError::ProtocolVersionMismatch);
2019        }
2020
2021        // Decrypt the encrypted_group_secrets using HPKE with the algorithms indicated by the
2022        // cipher suite and the HPKE private key corresponding to the GroupSecrets. If a
2023        // PreSharedKeyID is part of the GroupSecrets and the client is not in possession of
2024        // the corresponding PSK, return an error
2025        let group_secrets = GroupSecrets::decrypt(
2026            &cipher_suite_provider,
2027            &key_package_generation.init_secret_key,
2028            &key_package_generation.key_package.hpke_init_key,
2029            &welcome.encrypted_group_info,
2030            &encrypted_group_secrets.encrypted_group_secrets,
2031        )
2032        .await?;
2033
2034        let psk_secret = Self::psk_secret(
2035            config,
2036            &cipher_suite_provider,
2037            &group_secrets.psks,
2038            #[cfg(feature = "psk")]
2039            additional_psk,
2040        )
2041        .await?;
2042
2043        // From the joiner_secret in the decrypted GroupSecrets object and the PSKs specified in
2044        // the GroupSecrets, derive the welcome_secret and using that the welcome_key and
2045        // welcome_nonce.
2046        let welcome_secret = WelcomeSecret::from_joiner_secret(
2047            &cipher_suite_provider,
2048            &group_secrets.joiner_secret,
2049            &psk_secret,
2050        )
2051        .await?;
2052
2053        // Use the key and nonce to decrypt the encrypted_group_info field.
2054        let decrypted_group_info = welcome_secret
2055            .decrypt(&welcome.encrypted_group_info)
2056            .await?;
2057
2058        let group_info = GroupInfo::mls_decode(&mut &**decrypted_group_info)?;
2059
2060        Ok((
2061            group_info,
2062            key_package_generation,
2063            group_secrets,
2064            psk_secret,
2065        ))
2066    }
2067
2068    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
2069    #[cfg(feature = "prior_epoch")]
2070    pub(crate) async fn insert_past_epoch(&mut self) -> Result<(), MlsError> {
2071        let signature_public_keys = self
2072            .state
2073            .public_tree
2074            .leaves()
2075            .map(|l| l.map(|n| n.signing_identity.signature_key.clone()))
2076            .collect();
2077
2078        let past_epoch = PriorEpoch {
2079            context: self.context().clone(),
2080            self_index: self.private_tree.self_index,
2081            secrets: self.epoch_secrets.clone(),
2082            signature_public_keys,
2083            #[cfg(feature = "prior_epoch_membership_key")]
2084            membership_key: self.key_schedule.membership_key.to_vec(),
2085        };
2086
2087        self.state_repo.insert(past_epoch).await?;
2088
2089        Ok(())
2090    }
2091
2092    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
2093    #[cfg(not(feature = "prior_epoch"))]
2094    pub(crate) async fn insert_past_epoch(&mut self) -> Result<(), MlsError> {
2095        Ok(())
2096    }
2097}
2098
2099#[cfg(feature = "private_message")]
2100impl<C> GroupStateProvider for Group<C>
2101where
2102    C: ClientConfig + Clone,
2103{
2104    fn group_context(&self) -> &GroupContext {
2105        self.context()
2106    }
2107
2108    fn self_index(&self) -> LeafIndex {
2109        self.private_tree.self_index
2110    }
2111
2112    fn epoch_secrets_mut(&mut self) -> &mut EpochSecrets {
2113        &mut self.epoch_secrets
2114    }
2115
2116    fn epoch_secrets(&self) -> &EpochSecrets {
2117        &self.epoch_secrets
2118    }
2119}
2120
2121#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
2122#[cfg_attr(all(target_arch = "wasm32", mls_build_async), maybe_async::must_be_async(?Send))]
2123#[cfg_attr(
2124    all(not(target_arch = "wasm32"), mls_build_async),
2125    maybe_async::must_be_async
2126)]
2127impl<C> MessageProcessor for Group<C>
2128where
2129    C: ClientConfig + Clone,
2130{
2131    type MlsRules = C::MlsRules;
2132    type IdentityProvider = C::IdentityProvider;
2133    type PreSharedKeyStorage = C::PskStore;
2134    type OutputType = ReceivedMessage;
2135    type CipherSuiteProvider = <C::CryptoProvider as CryptoProvider>::CipherSuiteProvider;
2136
2137    #[cfg(feature = "private_message")]
2138    async fn process_ciphertext(
2139        &mut self,
2140        cipher_text: &PrivateMessage,
2141    ) -> Result<EventOrContent<Self::OutputType>, MlsError> {
2142        self.decrypt_incoming_ciphertext(cipher_text)
2143            .await
2144            .map(EventOrContent::Content)
2145    }
2146
2147    async fn verify_plaintext_authentication(
2148        &self,
2149        message: PublicMessage,
2150    ) -> Result<EventOrContent<Self::OutputType>, MlsError> {
2151        let auth_content = verify_plaintext_authentication(
2152            &self.cipher_suite_provider,
2153            message,
2154            Some(&self.key_schedule.membership_key),
2155            &self.state.context,
2156            SignaturePublicKeysContainer::RatchetTree(&self.state.public_tree),
2157        )
2158        .await?;
2159
2160        Ok(EventOrContent::Content(auth_content))
2161    }
2162
2163    #[cfg(all(feature = "export_key_generation", feature = "private_message"))]
2164    /// Returns the unauthenticated key generation used to decrypt the private message.
2165    async fn get_unauthenticated_key_generation_from_sender_data(
2166        &mut self,
2167        cipher_text: &PrivateMessage,
2168    ) -> Result<Option<u32>, MlsError> {
2169        let epoch_id = cipher_text.epoch;
2170        let sender_data = if epoch_id == self.context().epoch {
2171            CiphertextProcessor::new(self, self.cipher_suite_provider.clone())
2172                .open_sender_data(cipher_text)
2173                .await?
2174        } else {
2175            #[cfg(feature = "prior_epoch")]
2176            {
2177                let epoch = self
2178                    .state_repo
2179                    .get_epoch_mut(epoch_id)
2180                    .await?
2181                    .ok_or(MlsError::EpochNotFound)?;
2182                CiphertextProcessor::new(epoch, self.cipher_suite_provider.clone())
2183                    .open_sender_data(cipher_text)
2184                    .await?
2185            }
2186            #[cfg(not(feature = "prior_epoch"))]
2187            Err(MlsError::EpochNotFound)
2188        };
2189        Ok(Some(sender_data.generation))
2190    }
2191
2192    async fn apply_update_path(
2193        &mut self,
2194        sender: LeafIndex,
2195        update_path: &ValidatedUpdatePath,
2196        provisional_state: &mut ProvisionalState,
2197    ) -> Result<Option<(TreeKemPrivate, PathSecret)>, MlsError> {
2198        // Update the private tree to create a provisional private tree
2199        let (mut provisional_private_tree, new_signer) =
2200            self.provisional_private_tree(provisional_state)?;
2201
2202        if let Some(signer) = new_signer {
2203            self.signer = signer;
2204        }
2205
2206        provisional_state
2207            .public_tree
2208            .apply_update_path(
2209                sender,
2210                update_path,
2211                &provisional_state.group_context.extensions,
2212                self.identity_provider(),
2213                self.cipher_suite_provider(),
2214            )
2215            .await?;
2216
2217        if sender == self.private_tree.self_index {
2218            let PendingCommitSnapshot::LegacyPendingCommit(legacy_pending) = &self.pending_commit
2219            else {
2220                return Err(MlsError::CantProcessMessageFromSelf);
2221            };
2222
2223            return Ok(Some((
2224                legacy_pending.private_tree.clone(),
2225                legacy_pending.commit_secret.clone(),
2226            )));
2227        }
2228
2229        // Update the tree hash to get context for decryption
2230        provisional_state.group_context.tree_hash = provisional_state
2231            .public_tree
2232            .tree_hash(&self.cipher_suite_provider)
2233            .await?;
2234
2235        let context_bytes = provisional_state.group_context.mls_encode_to_vec()?;
2236
2237        TreeKem::new(
2238            &mut provisional_state.public_tree,
2239            &mut provisional_private_tree,
2240        )
2241        .decap(
2242            sender,
2243            update_path,
2244            &provisional_state.indexes_of_added_kpkgs,
2245            &context_bytes,
2246            &self.cipher_suite_provider,
2247        )
2248        .await
2249        .map(|root_secret| Some((provisional_private_tree, root_secret)))
2250    }
2251
2252    async fn update_key_schedule(
2253        &mut self,
2254        secrets: Option<(TreeKemPrivate, PathSecret)>,
2255        interim_transcript_hash: InterimTranscriptHash,
2256        confirmation_tag: &ConfirmationTag,
2257        provisional_state: ProvisionalState,
2258    ) -> Result<(), MlsError> {
2259        let commit_secret = if let Some(secrets) = secrets {
2260            self.private_tree = secrets.0;
2261            secrets.1
2262        } else {
2263            PathSecret::empty(&self.cipher_suite_provider)
2264        };
2265
2266        // Use the commit_secret, the psk_secret, the provisional GroupContext, and the init secret
2267        // from the previous epoch (or from the external init) to compute the epoch secret and
2268        // derived secrets for the new epoch
2269
2270        let key_schedule = match provisional_state
2271            .applied_proposals
2272            .external_initializations
2273            .first()
2274        {
2275            Some(ext_init) => {
2276                self.key_schedule
2277                    .derive_for_external(&ext_init.proposal.kem_output, &self.cipher_suite_provider)
2278                    .await?
2279            }
2280            _ => self.key_schedule.clone(),
2281        };
2282
2283        #[cfg(feature = "psk")]
2284        let (psk, _) = self
2285            .get_psk(&provisional_state.applied_proposals.psks)
2286            .await?;
2287
2288        #[cfg(not(feature = "psk"))]
2289        let psk = self.get_psk();
2290
2291        let key_schedule_result = KeySchedule::from_key_schedule(
2292            &key_schedule,
2293            &commit_secret,
2294            &provisional_state.group_context,
2295            #[cfg(any(feature = "secret_tree_access", feature = "private_message"))]
2296            provisional_state.public_tree.total_leaf_count(),
2297            &psk,
2298            &self.cipher_suite_provider,
2299        )
2300        .await?;
2301
2302        // Use the confirmation_key for the new epoch to compute the confirmation tag for
2303        // this message, as described below, and verify that it is the same as the
2304        // confirmation_tag field in the MlsPlaintext object.
2305        let new_confirmation_tag = ConfirmationTag::create(
2306            &key_schedule_result.confirmation_key,
2307            &provisional_state.group_context.confirmed_transcript_hash,
2308            &self.cipher_suite_provider,
2309        )
2310        .await?;
2311
2312        if &new_confirmation_tag != confirmation_tag {
2313            return Err(MlsError::InvalidConfirmationTag);
2314        }
2315
2316        self.insert_past_epoch().await?;
2317
2318        self.epoch_secrets = key_schedule_result.epoch_secrets;
2319        self.state.context = provisional_state.group_context;
2320        self.state.interim_transcript_hash = interim_transcript_hash;
2321        self.key_schedule = key_schedule_result.key_schedule;
2322        self.state.public_tree = provisional_state.public_tree;
2323        self.state.confirmation_tag = new_confirmation_tag;
2324
2325        // Clear the proposals list
2326        #[cfg(feature = "by_ref_proposal")]
2327        self.state.proposals.clear();
2328
2329        // Clear the pending updates list
2330        #[cfg(feature = "by_ref_proposal")]
2331        {
2332            self.pending_updates = Default::default();
2333        }
2334
2335        self.pending_commit = Default::default();
2336
2337        Ok(())
2338    }
2339
2340    fn mls_rules(&self) -> Self::MlsRules {
2341        self.config.mls_rules()
2342    }
2343
2344    fn identity_provider(&self) -> Self::IdentityProvider {
2345        self.config.identity_provider()
2346    }
2347
2348    fn psk_storage(&self) -> Self::PreSharedKeyStorage {
2349        self.config.secret_store()
2350    }
2351
2352    fn group_state(&self) -> &GroupState {
2353        &self.state
2354    }
2355
2356    fn group_state_mut(&mut self) -> &mut GroupState {
2357        &mut self.state
2358    }
2359
2360    fn removal_proposal(
2361        &self,
2362        provisional_state: &ProvisionalState,
2363    ) -> Option<ProposalInfo<RemoveProposal>> {
2364        provisional_state
2365            .applied_proposals
2366            .removals
2367            .iter()
2368            .find(|p| p.proposal.to_remove == self.private_tree.self_index)
2369            .cloned()
2370    }
2371
2372    #[cfg(all(
2373        feature = "by_ref_proposal",
2374        feature = "custom_proposal",
2375        feature = "self_remove_proposal"
2376    ))]
2377    #[cfg_attr(feature = "ffi", safer_ffi_gen::safer_ffi_gen_ignore)]
2378    fn self_removal_proposal(
2379        &self,
2380        provisional_state: &ProvisionalState,
2381    ) -> Option<ProposalInfo<SelfRemoveProposal>> {
2382        provisional_state
2383            .applied_proposals
2384            .self_removes
2385            .iter()
2386            .find(|p| p.sender == Sender::Member(*self.private_tree.self_index))
2387            .cloned()
2388    }
2389
2390    #[cfg(feature = "private_message")]
2391    fn min_epoch_available(&self) -> Option<u64> {
2392        None
2393    }
2394
2395    fn cipher_suite_provider(&self) -> &Self::CipherSuiteProvider {
2396        &self.cipher_suite_provider
2397    }
2398}
2399
2400#[cfg(test)]
2401pub(crate) mod test_utils;
2402
2403#[cfg(test)]
2404mod tests {
2405    use crate::{
2406        client::test_utils::{
2407            test_client_with_key_pkg, TestClientBuilder, TEST_CIPHER_SUITE, TEST_PROTOCOL_VERSION,
2408        },
2409        client_builder::test_utils::TestClientConfig,
2410        crypto::test_utils::TestCryptoProvider,
2411        group::proposal_filter::ProposalInfo,
2412        identity::test_utils::get_test_signing_identity,
2413        key_package::test_utils::test_key_package_message,
2414        mls_rules::CommitOptions,
2415        tree_kem::{
2416            leaf_node::{test_utils::get_test_capabilities, LeafNodeSource},
2417            UpdatePathNode,
2418        },
2419    };
2420
2421    #[cfg(feature = "by_ref_proposal")]
2422    use crate::{
2423        client::test_utils::{test_client_with_key_pkg_custom, TEST_CUSTOM_PROPOSAL_TYPE},
2424        client_builder::{ClientBuilder, MlsConfig},
2425        group::{
2426            component_operation::ComponentID,
2427            mls_rules::{CommitDirection, CommitSource},
2428            proposal_filter::ProposalBundle,
2429        },
2430        identity::basic::BasicIdentityProvider,
2431        identity::test_utils::BasicWithCustomProvider,
2432    };
2433
2434    #[cfg(any(feature = "private_message", feature = "custom_proposal"))]
2435    use crate::group::mls_rules::DefaultMlsRules;
2436
2437    #[cfg(feature = "prior_epoch")]
2438    use crate::group::padding::PaddingMode;
2439
2440    use crate::{extension::RequiredCapabilitiesExt, key_package::test_utils::test_key_package};
2441
2442    #[cfg(all(feature = "by_ref_proposal", feature = "custom_proposal"))]
2443    use super::test_utils::test_group_custom_config;
2444
2445    #[cfg(any(feature = "psk", feature = "std"))]
2446    use crate::client::Client;
2447
2448    #[cfg(feature = "psk")]
2449    use crate::psk::PreSharedKey;
2450
2451    #[cfg(any(feature = "by_ref_proposal", feature = "private_message"))]
2452    use crate::group::test_utils::random_bytes;
2453
2454    #[cfg(feature = "by_ref_proposal")]
2455    use crate::{
2456        extension::test_utils::TestExtension, identity::test_utils::get_test_basic_credential,
2457        time::MlsTime,
2458    };
2459
2460    use super::{
2461        test_utils::{
2462            get_test_25519_key, get_test_groups_with_features, group_extensions, process_commit,
2463            test_group, test_group_custom, test_n_member_group, TestGroup, TEST_GROUP,
2464        },
2465        *,
2466    };
2467
2468    use assert_matches::assert_matches;
2469
2470    use message_processor::CommitEffect;
2471    use mls_rs_core::extension::{Extension, ExtensionType};
2472    use mls_rs_core::identity::{Credential, CredentialType, CustomCredential};
2473
2474    #[cfg(feature = "by_ref_proposal")]
2475    use mls_rs_core::identity::CertificateChain;
2476
2477    #[cfg(feature = "by_ref_proposal")]
2478    use crate::{crypto::test_utils::test_cipher_suite_provider, extension::ExternalSendersExt};
2479
2480    #[cfg(feature = "private_message")]
2481    use super::test_utils::test_member;
2482
2483    use mls_rs_core::extension::MlsExtension;
2484
2485    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
2486    async fn test_create_group() {
2487        for (protocol_version, cipher_suite) in ProtocolVersion::all().flat_map(|p| {
2488            TestCryptoProvider::all_supported_cipher_suites()
2489                .into_iter()
2490                .map(move |cs| (p, cs))
2491        }) {
2492            let group = test_group(protocol_version, cipher_suite).await;
2493
2494            assert_eq!(group.cipher_suite(), cipher_suite);
2495            assert_eq!(group.state.context.epoch, 0);
2496            assert_eq!(group.state.context.group_id, TEST_GROUP.to_vec());
2497            assert_eq!(group.state.context.extensions, group_extensions());
2498
2499            assert_eq!(
2500                group.state.context.confirmed_transcript_hash,
2501                ConfirmedTranscriptHash::from(vec![])
2502            );
2503
2504            #[cfg(feature = "private_message")]
2505            assert!(group.state.proposals.is_empty());
2506
2507            #[cfg(feature = "by_ref_proposal")]
2508            assert!(group.pending_updates.is_empty());
2509
2510            assert!(!group.has_pending_commit());
2511
2512            assert_eq!(*group.private_tree.self_index, group.current_member_index());
2513        }
2514    }
2515
2516    #[cfg(feature = "private_message")]
2517    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
2518    async fn test_pending_proposals_application_data() {
2519        let mut test_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
2520
2521        // Create a proposal
2522        let (bob_key_package, _) =
2523            test_member(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, b"bob").await;
2524
2525        let proposal = test_group
2526            .add_proposal(bob_key_package.key_package_message())
2527            .unwrap();
2528
2529        test_group.proposal_message(proposal, vec![]).await.unwrap();
2530
2531        // We should not be able to send application messages until a commit happens
2532        let res = test_group
2533            .encrypt_application_message(b"test", vec![])
2534            .await;
2535
2536        assert_matches!(res, Err(MlsError::CommitRequired));
2537
2538        // We should be able to send application messages after a commit
2539        test_group.commit(vec![]).await.unwrap();
2540
2541        assert!(test_group.has_pending_commit());
2542
2543        test_group.apply_pending_commit().await.unwrap();
2544
2545        let res = test_group
2546            .encrypt_application_message(b"test", vec![])
2547            .await;
2548
2549        assert!(res.is_ok());
2550    }
2551
2552    #[cfg(feature = "by_ref_proposal")]
2553    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
2554    async fn test_update_proposals() {
2555        let new_extension = TestExtension { foo: 10 };
2556        let mut extension_list = ExtensionList::default();
2557        extension_list.set_from(new_extension).unwrap();
2558
2559        let mut test_group = test_group_custom(
2560            TEST_PROTOCOL_VERSION,
2561            TEST_CIPHER_SUITE,
2562            vec![42.into()],
2563            Some(extension_list.clone()),
2564            None,
2565        )
2566        .await;
2567
2568        let existing_leaf = test_group.current_user_leaf_node().unwrap().clone();
2569
2570        // Create an update proposal
2571        let proposal = test_group.update_proposal().await;
2572
2573        let update = match proposal {
2574            Proposal::Update(update) => update,
2575            _ => panic!("non update proposal found"),
2576        };
2577
2578        assert_ne!(update.leaf_node.public_key, existing_leaf.public_key);
2579
2580        assert_eq!(
2581            update.leaf_node.signing_identity,
2582            existing_leaf.signing_identity
2583        );
2584
2585        assert_eq!(update.leaf_node.ungreased_extensions(), extension_list);
2586        assert_eq!(
2587            update.leaf_node.ungreased_capabilities().sorted(),
2588            Capabilities {
2589                extensions: vec![42.into()],
2590                ..get_test_capabilities()
2591            }
2592            .sorted()
2593        );
2594    }
2595
2596    #[cfg(feature = "by_ref_proposal")]
2597    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
2598    async fn test_invalid_commit_self_update() {
2599        let mut test_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
2600
2601        // Create an update proposal
2602        let proposal_msg = test_group.propose_update(vec![]).await.unwrap();
2603
2604        let proposal = match proposal_msg.into_plaintext().unwrap().content.content {
2605            Content::Proposal(p) => p,
2606            _ => panic!("found non-proposal message"),
2607        };
2608
2609        let update_leaf = match *proposal {
2610            Proposal::Update(u) => u.leaf_node,
2611            _ => panic!("found proposal message that isn't an update"),
2612        };
2613
2614        test_group.commit(vec![]).await.unwrap();
2615        test_group.apply_pending_commit().await.unwrap();
2616
2617        // The leaf node should not be the one from the update, because the committer rejects it
2618        assert_ne!(&update_leaf, test_group.current_user_leaf_node().unwrap());
2619    }
2620
2621    #[cfg(feature = "by_ref_proposal")]
2622    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
2623    async fn update_proposal_with_bad_key_package_is_ignored_when_committing() {
2624        let (mut alice_group, mut bob_group) =
2625            test_two_member_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, true).await;
2626
2627        let mut proposal = alice_group.update_proposal().await;
2628
2629        if let Proposal::Update(ref mut update) = proposal {
2630            update.leaf_node.signature = random_bytes(32);
2631        } else {
2632            panic!("Invalid update proposal")
2633        }
2634
2635        let proposal_message = alice_group
2636            .proposal_message(proposal.clone(), vec![])
2637            .await
2638            .unwrap();
2639
2640        let proposal_plaintext = match proposal_message.payload {
2641            MlsMessagePayload::Plain(p) => p,
2642            _ => panic!("Unexpected non-plaintext message"),
2643        };
2644
2645        let proposal_ref = ProposalRef::from_content(
2646            &bob_group.cipher_suite_provider,
2647            &proposal_plaintext.clone().into(),
2648        )
2649        .await
2650        .unwrap();
2651
2652        // Hack bob's receipt of the proposal
2653        bob_group
2654            .state
2655            .proposals
2656            .insert(proposal_ref, proposal, proposal_plaintext.content.sender);
2657
2658        let commit_output = bob_group.commit(vec![]).await.unwrap();
2659
2660        assert_matches!(
2661            commit_output.commit_message,
2662            MlsMessage {
2663                payload: MlsMessagePayload::Plain(
2664                    PublicMessage {
2665                        content: FramedContent {
2666                            content: Content::Commit(c),
2667                            ..
2668                        },
2669                        ..
2670                    }),
2671                ..
2672            } if c.proposals.is_empty()
2673        );
2674    }
2675
2676    #[cfg(feature = "non_domain_separated_hpke_encrypt_decrypt")]
2677    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
2678    async fn test_hpke_encrypt_decrypt() {
2679        let (alice_group, bob_group) =
2680            test_two_member_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, true).await;
2681        let receiver_index = alice_group.current_member_index();
2682        let sender_index = bob_group.current_member_index();
2683
2684        let context_info: Vec<u8> = vec![
2685            receiver_index.try_into().unwrap(),
2686            sender_index.try_into().unwrap(),
2687        ];
2688        let plaintext = b"message";
2689
2690        let hpke_ciphertext = bob_group
2691            .hpke_encrypt_to_recipient(receiver_index, &context_info, None, plaintext)
2692            .await
2693            .unwrap();
2694        let hpke_decrypted = alice_group
2695            .hpke_decrypt_for_current_member(&context_info, None, hpke_ciphertext)
2696            .await
2697            .unwrap();
2698
2699        assert_eq!(plaintext.to_vec(), hpke_decrypted);
2700    }
2701
2702    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
2703    async fn safe_context_test_hpke_encrypt_decrypt() {
2704        let component_id: ComponentID = 1;
2705        let (alice_group, bob_group) =
2706            test_two_member_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, true).await;
2707        let receiver_index = alice_group.current_member_index();
2708        let sender_index = bob_group.current_member_index();
2709
2710        let context_info: Vec<u8> = vec![
2711            receiver_index.try_into().unwrap(),
2712            sender_index.try_into().unwrap(),
2713        ];
2714        let plaintext = b"message";
2715
2716        let hpke_ciphertext = bob_group
2717            .safe_encrypt_with_context_to_recipient(
2718                receiver_index,
2719                component_id,
2720                &context_info,
2721                None,
2722                plaintext,
2723            )
2724            .await
2725            .unwrap();
2726        let hpke_decrypted = alice_group
2727            .safe_decrypt_with_context_for_current_member(
2728                component_id,
2729                &context_info,
2730                None,
2731                hpke_ciphertext,
2732            )
2733            .await
2734            .unwrap();
2735
2736        assert_eq!(plaintext.to_vec(), hpke_decrypted);
2737    }
2738
2739    #[cfg(feature = "non_domain_separated_hpke_encrypt_decrypt")]
2740    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
2741    async fn test_hpke_non_recipient_cant_decrypt() {
2742        let mut alice = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
2743        let (mut bob, _) = alice.join("bob").await;
2744        let (carol, commit) = alice.join("carol").await;
2745
2746        // Apply the commit that adds carol
2747        bob.process_incoming_message(commit).await.unwrap();
2748
2749        let receiver_index = alice.current_member_index();
2750        let sender_index = bob.current_member_index();
2751
2752        let context_info: Vec<u8> = vec![
2753            receiver_index.try_into().unwrap(),
2754            sender_index.try_into().unwrap(),
2755        ];
2756        let plaintext = b"message";
2757
2758        let hpke_ciphertext = bob
2759            .hpke_encrypt_to_recipient(receiver_index, &context_info, None, plaintext)
2760            .await
2761            .unwrap();
2762
2763        // different recipient tries to decrypt
2764        let hpke_decrypted = carol
2765            .hpke_decrypt_for_current_member(&context_info, None, hpke_ciphertext)
2766            .await;
2767
2768        // should fail because carol can't decrypt the message encrypted for alice
2769        assert_matches!(hpke_decrypted, Err(MlsError::CryptoProviderError(_)));
2770    }
2771
2772    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
2773    async fn safe_context_test_hpke_non_recipient_cant_decrypt() {
2774        let component_id: ComponentID = 345;
2775        let mut alice = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
2776        let (mut bob, _) = alice.join("bob").await;
2777        let (carol, commit) = alice.join("carol").await;
2778
2779        // Apply the commit that adds carol
2780        bob.process_incoming_message(commit).await.unwrap();
2781
2782        let receiver_index = alice.current_member_index();
2783        let sender_index = bob.current_member_index();
2784
2785        let context_info: Vec<u8> = vec![
2786            receiver_index.try_into().unwrap(),
2787            sender_index.try_into().unwrap(),
2788        ];
2789        let plaintext = b"message";
2790
2791        let hpke_ciphertext = bob
2792            .safe_encrypt_with_context_to_recipient(
2793                receiver_index,
2794                component_id,
2795                &context_info,
2796                None,
2797                plaintext,
2798            )
2799            .await
2800            .unwrap();
2801
2802        // different recipient tries to decrypt
2803        let hpke_decrypted = carol
2804            .safe_decrypt_with_context_for_current_member(
2805                component_id,
2806                &context_info,
2807                None,
2808                hpke_ciphertext,
2809            )
2810            .await;
2811
2812        // should fail because carol can't decrypt the message encrypted for alice
2813        assert_matches!(hpke_decrypted, Err(MlsError::CryptoProviderError(_)));
2814    }
2815
2816    #[cfg(feature = "non_domain_separated_hpke_encrypt_decrypt")]
2817    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
2818    async fn test_hpke_can_decrypt_after_group_changes() {
2819        let mut alice = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
2820        let (mut bob, _) = alice.join("bob").await;
2821
2822        let receiver_index = alice.current_member_index();
2823        let sender_index = bob.current_member_index();
2824        let context_info: Vec<u8> = vec![
2825            receiver_index.try_into().unwrap(),
2826            sender_index.try_into().unwrap(),
2827        ];
2828        let associated_data: Vec<u8> = vec![1, 2, 3, 4];
2829        let plaintext = b"message";
2830
2831        // encrypt the message to alice
2832        let hpke_ciphertext = bob
2833            .hpke_encrypt_to_recipient(
2834                receiver_index,
2835                &context_info,
2836                Some(&associated_data),
2837                plaintext,
2838            )
2839            .await
2840            .unwrap();
2841
2842        // add carol to the group
2843        let (_carol, commit) = alice.join("carol").await;
2844        bob.process_incoming_message(commit).await.unwrap();
2845
2846        // make sure alice can still decrypt
2847        let hpke_decrypted = alice
2848            .hpke_decrypt_for_current_member(&context_info, Some(&associated_data), hpke_ciphertext)
2849            .await
2850            .unwrap();
2851        assert_eq!(plaintext.to_vec(), hpke_decrypted);
2852    }
2853
2854    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
2855    async fn safe_context_test_hpke_can_decrypt_after_group_changes() {
2856        let component_id: ComponentID = 2;
2857        let mut alice = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
2858        let (mut bob, _) = alice.join("bob").await;
2859
2860        let receiver_index = alice.current_member_index();
2861        let sender_index = bob.current_member_index();
2862        let context_info: Vec<u8> = vec![
2863            receiver_index.try_into().unwrap(),
2864            sender_index.try_into().unwrap(),
2865        ];
2866        let associated_data: Vec<u8> = vec![1, 2, 3, 4];
2867        let plaintext = b"message";
2868
2869        // encrypt the message to alice
2870        let hpke_ciphertext = bob
2871            .safe_encrypt_with_context_to_recipient(
2872                receiver_index,
2873                component_id,
2874                &context_info,
2875                Some(&associated_data),
2876                plaintext,
2877            )
2878            .await
2879            .unwrap();
2880
2881        // add carol to the group
2882        let (_carol, commit) = alice.join("carol").await;
2883        bob.process_incoming_message(commit).await.unwrap();
2884
2885        // make sure alice can still decrypt
2886        let hpke_decrypted = alice
2887            .safe_decrypt_with_context_for_current_member(
2888                component_id,
2889                &context_info,
2890                Some(&associated_data),
2891                hpke_ciphertext,
2892            )
2893            .await
2894            .unwrap();
2895        assert_eq!(plaintext.to_vec(), hpke_decrypted);
2896    }
2897
2898    #[cfg(all(
2899        feature = "prior_epoch",
2900        feature = "custom_proposal",
2901        feature = "by_ref_proposal",
2902        feature = "prior_epoch_membership_key"
2903    ))]
2904    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
2905    async fn test_can_validate_and_get_data_custom_proposal_from_past_epoch() {
2906        let mut alice = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
2907        let (mut bob, _) = alice.join("bob").await;
2908
2909        let data = vec![1, 2, 3];
2910        let custom_proposal = CustomProposal::new(TEST_CUSTOM_PROPOSAL_TYPE, vec![4, 5, 6]);
2911        let proposal = alice
2912            .propose_custom(custom_proposal.clone(), data.clone())
2913            .await
2914            .unwrap();
2915
2916        // add carol to the group
2917        let (_carol, commit) = alice.join("carol").await;
2918        bob.process_incoming_message(commit).await.unwrap();
2919        let (validated_data, sender) = bob
2920            .validate_custom_proposal(&proposal, Some(TEST_CUSTOM_PROPOSAL_TYPE))
2921            .await
2922            .unwrap();
2923        assert_eq!(data, validated_data);
2924        assert_eq!(sender, Sender::Member(0));
2925    }
2926
2927    #[cfg(all(
2928        feature = "prior_epoch",
2929        feature = "custom_proposal",
2930        feature = "by_ref_proposal",
2931        feature = "prior_epoch_membership_key"
2932    ))]
2933    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
2934    async fn test_can_validate_and_get_data_custom_proposal_from_current_epoch() {
2935        let mut alice = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
2936        let (mut bob, _) = alice.join("bob").await;
2937
2938        let data = vec![1, 2, 3];
2939        let custom_proposal = CustomProposal::new(TEST_CUSTOM_PROPOSAL_TYPE, vec![3, 4, 5]);
2940        let proposal = alice
2941            .propose_custom(custom_proposal.clone(), data.clone())
2942            .await
2943            .unwrap();
2944
2945        let (validated_data, sender) = bob.validate_custom_proposal(&proposal, None).await.unwrap();
2946        assert_eq!(data, validated_data);
2947        assert_eq!(sender, Sender::Member(0));
2948    }
2949
2950    #[cfg(all(feature = "prior_epoch", feature = "prior_epoch_membership_key"))]
2951    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
2952    async fn test_can_validate_non_custom_proposal_from_past_epoch() {
2953        let mut alice = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
2954        let (mut bob, _) = alice.join("bob").await;
2955
2956        let proposal = alice.propose_update(Vec::new()).await.unwrap();
2957
2958        // add carol to the group
2959        let (_carol, commit) = alice.join("carol").await;
2960        bob.process_incoming_message(commit).await.unwrap();
2961
2962        bob.validate_public_message(&proposal).await.unwrap();
2963    }
2964
2965    #[cfg(all(
2966        feature = "prior_epoch",
2967        feature = "by_ref_proposal",
2968        feature = "prior_epoch_membership_key"
2969    ))]
2970    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
2971    async fn test_cannot_validate_custom_proposal_for_non_custom_proposal() {
2972        let mut alice = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
2973        let (mut bob, _) = alice.join("bob").await;
2974
2975        let proposal = alice.propose_update(Vec::new()).await.unwrap();
2976
2977        // add carol to the group
2978        let (_carol, commit) = alice.join("carol").await;
2979        bob.process_incoming_message(commit).await.unwrap();
2980
2981        let data_err = bob.validate_custom_proposal(&proposal, None).await;
2982        assert_matches!(data_err, Err(MlsError::UnexpectedMessageType));
2983    }
2984
2985    #[cfg(all(feature = "prior_epoch", feature = "prior_epoch_membership_key"))]
2986    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
2987    async fn test_can_validate_commit_from_past_epoch() {
2988        let mut alice = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
2989        let (mut bob, _) = alice.join("bob").await;
2990
2991        let old_commit_output = bob
2992            .commit_builder()
2993            .remove_member(0)
2994            .unwrap()
2995            .build()
2996            .await
2997            .unwrap();
2998
2999        // add carol to the group
3000        let (_carol, commit) = alice.join("carol").await;
3001        bob.process_incoming_message(commit).await.unwrap();
3002
3003        bob.validate_public_message(&old_commit_output.commit_message)
3004            .await
3005            .unwrap();
3006    }
3007
3008    #[cfg(all(
3009        feature = "prior_epoch",
3010        feature = "custom_proposal",
3011        feature = "prior_epoch_membership_key"
3012    ))]
3013    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
3014    async fn test_can_validate_custom_proposal_from_current_epoch() {
3015        let mut alice = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
3016        let (mut bob, _) = alice.join("bob").await;
3017
3018        let custom_proposal = CustomProposal::new(TEST_CUSTOM_PROPOSAL_TYPE, vec![0, 1, 2]);
3019        let proposal = alice
3020            .propose_custom(custom_proposal.clone(), vec![])
3021            .await
3022            .unwrap();
3023
3024        bob.validate_public_message(&proposal).await.unwrap();
3025    }
3026
3027    #[cfg(all(feature = "prior_epoch", feature = "prior_epoch_membership_key"))]
3028    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
3029    async fn test_cannot_validate_non_public_message() {
3030        let mut alice = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
3031        let (mut bob, _) = alice.join("bob").await;
3032
3033        let res = alice
3034            .encrypt_application_message(b"test", vec![])
3035            .await
3036            .unwrap();
3037
3038        let auth_content = bob.validate_public_message(&res).await;
3039        assert_matches!(auth_content, Err(MlsError::UnexpectedMessageType));
3040    }
3041
3042    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
3043    async fn test_two_member_group(
3044        protocol_version: ProtocolVersion,
3045        cipher_suite: CipherSuite,
3046        tree_ext: bool,
3047    ) -> (TestGroup, TestGroup) {
3048        let mut test_group = test_group_custom(
3049            protocol_version,
3050            cipher_suite,
3051            Default::default(),
3052            None,
3053            Some(CommitOptions::new().with_ratchet_tree_extension(tree_ext)),
3054        )
3055        .await;
3056
3057        let (bob_test_group, _) = test_group.join("bob").await;
3058
3059        assert!(Group::equal_group_state(&test_group, &bob_test_group));
3060
3061        (test_group, bob_test_group)
3062    }
3063
3064    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
3065    async fn test_welcome_processing_exported_tree() {
3066        test_two_member_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, false).await;
3067    }
3068
3069    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
3070    async fn test_welcome_processing_tree_extension() {
3071        test_two_member_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, true).await;
3072    }
3073
3074    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
3075    async fn test_welcome_processing_missing_tree() {
3076        let mut test_group = test_group_custom(
3077            TEST_PROTOCOL_VERSION,
3078            TEST_CIPHER_SUITE,
3079            Default::default(),
3080            None,
3081            Some(CommitOptions::new().with_ratchet_tree_extension(false)),
3082        )
3083        .await;
3084
3085        let (bob_client, bob_key_package) =
3086            test_client_with_key_pkg(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "bob").await;
3087
3088        // Add bob to the group
3089        let commit_output = test_group
3090            .commit_builder()
3091            .add_member(bob_key_package)
3092            .unwrap()
3093            .build()
3094            .await
3095            .unwrap();
3096
3097        // Group from Bob's perspective
3098        let bob_group = Group::join(
3099            &commit_output.welcome_messages[0],
3100            None,
3101            bob_client.config,
3102            bob_client.signer.unwrap(),
3103            None,
3104        )
3105        .await
3106        .map(|_| ());
3107
3108        assert_matches!(bob_group, Err(MlsError::RatchetTreeNotFound));
3109    }
3110
3111    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
3112    async fn test_reused_key_package() {
3113        let mut alice_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
3114        let (bob_client, bob_key_package) =
3115            test_client_with_key_pkg(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "bob").await;
3116        let mut carla_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
3117
3118        // Alice adds Bob to her group.
3119        let commit_output = alice_group
3120            .group
3121            .commit_builder()
3122            .add_member(bob_key_package.clone())
3123            .unwrap()
3124            .build()
3125            .await
3126            .unwrap();
3127
3128        // Bob joins group.
3129        let (mut bob_group, _) = bob_client
3130            .join_group(None, &commit_output.welcome_messages[0], None)
3131            .await
3132            .unwrap();
3133        // This deletes the key package used to join the group.
3134        bob_group.write_to_storage().await.unwrap();
3135
3136        // Carla adds Bob, reusing the same key package.
3137        let commit_output = carla_group
3138            .group
3139            .commit_builder()
3140            .add_member(bob_key_package.clone())
3141            .unwrap()
3142            .build()
3143            .await
3144            .unwrap();
3145
3146        // Bob cannot join Carla's group.
3147        let bob_group = bob_client
3148            .join_group(None, &commit_output.welcome_messages[0], None)
3149            .await
3150            .map(|_| ());
3151        assert_matches!(bob_group, Err(MlsError::WelcomeKeyPackageNotFound));
3152    }
3153
3154    #[cfg(feature = "last_resort_key_package_ext")]
3155    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
3156    async fn test_last_resort_key_package() -> Result<(), MlsError> {
3157        let mut alice_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
3158        let (bob_client, bob_key_package) = test_client_with_key_pkg_custom(
3159            TEST_PROTOCOL_VERSION,
3160            TEST_CIPHER_SUITE,
3161            "bob",
3162            vec![LastResortKeyPackageExt.into_extension().unwrap()].into(),
3163            Default::default(),
3164            |_| {},
3165        )
3166        .await;
3167        let mut carla_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
3168
3169        // Alice adds Bob to her group.
3170        let commit_output = alice_group
3171            .group
3172            .commit_builder()
3173            .add_member(bob_key_package.clone())?
3174            .build()
3175            .await?;
3176
3177        // Bob joins group.
3178        let (mut bob_group, _) = bob_client
3179            .join_group(None, &commit_output.welcome_messages[0], None)
3180            .await?;
3181        // This no longer deletes the key package
3182        bob_group.write_to_storage()?;
3183
3184        // Carla adds Bob, reusing the same key package.
3185        let commit_output = carla_group
3186            .group
3187            .commit_builder()
3188            .add_member(bob_key_package.clone())?
3189            .build()
3190            .await?;
3191
3192        // Bob can join Carla's group.
3193        bob_client
3194            .join_group(None, &commit_output.welcome_messages[0], None)
3195            .await?;
3196
3197        Ok(())
3198    }
3199
3200    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
3201    async fn test_group_context_ext_proposal_create() {
3202        let test_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
3203
3204        let mut extension_list = ExtensionList::new();
3205        extension_list
3206            .set_from(RequiredCapabilitiesExt {
3207                extensions: vec![42.into()],
3208                proposals: vec![],
3209                credentials: vec![],
3210            })
3211            .unwrap();
3212
3213        let proposal = test_group.group_context_extensions_proposal(extension_list.clone());
3214
3215        assert_matches!(proposal, Proposal::GroupContextExtensions(ext) if ext == extension_list);
3216    }
3217
3218    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
3219    async fn group_context_extension_proposal_test(
3220        ext_list: ExtensionList,
3221    ) -> (TestGroup, Result<MlsMessage, MlsError>) {
3222        let protocol_version = TEST_PROTOCOL_VERSION;
3223        let cipher_suite = TEST_CIPHER_SUITE;
3224
3225        let mut test_group =
3226            test_group_custom(protocol_version, cipher_suite, vec![42.into()], None, None).await;
3227
3228        let commit = test_group
3229            .commit_builder()
3230            .set_group_context_ext(ext_list)
3231            .unwrap()
3232            .build()
3233            .await
3234            .map(|commit_output| commit_output.commit_message);
3235
3236        (test_group, commit)
3237    }
3238
3239    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
3240    async fn test_group_context_ext_proposal_commit() {
3241        use message_processor::CommitEffect;
3242
3243        let mut extension_list = ExtensionList::new();
3244
3245        extension_list
3246            .set_from(RequiredCapabilitiesExt {
3247                extensions: vec![42.into()],
3248                proposals: vec![],
3249                credentials: vec![],
3250            })
3251            .unwrap();
3252
3253        let (mut test_group, _) =
3254            group_context_extension_proposal_test(extension_list.clone()).await;
3255
3256        let update = test_group.apply_pending_commit().await.unwrap();
3257
3258        // FIXME: Test Reporting
3259        assert_matches!(update.effect, CommitEffect::NewEpoch(_));
3260
3261        assert_eq!(test_group.state.context.extensions, extension_list)
3262    }
3263
3264    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
3265    async fn test_group_context_ext_proposal_invalid() {
3266        let mut extension_list = ExtensionList::new();
3267        extension_list
3268            .set_from(RequiredCapabilitiesExt {
3269                extensions: vec![999.into()],
3270                proposals: vec![],
3271                credentials: vec![],
3272            })
3273            .unwrap();
3274
3275        let (_, commit) = group_context_extension_proposal_test(extension_list.clone()).await;
3276
3277        assert_matches!(
3278            commit,
3279            Err(MlsError::RequiredExtensionNotFound(a)) if a == 999.into()
3280        );
3281    }
3282
3283    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
3284    async fn make_group_with_required_capabilities(
3285        required_caps: RequiredCapabilitiesExt,
3286    ) -> Result<Group<TestClientConfig>, MlsError> {
3287        test_client_with_key_pkg(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "alice")
3288            .await
3289            .0
3290            .create_group(
3291                core::iter::once(required_caps.into_extension().unwrap()).collect(),
3292                Default::default(),
3293                None,
3294            )
3295            .await
3296    }
3297
3298    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
3299    async fn creating_group_with_member_not_supporting_required_credential_type_fails() {
3300        let group_creation = make_group_with_required_capabilities(RequiredCapabilitiesExt {
3301            credentials: vec![CredentialType::BASIC, CredentialType::X509],
3302            ..Default::default()
3303        })
3304        .await
3305        .map(|_| ());
3306
3307        assert_matches!(
3308            group_creation,
3309            Err(MlsError::RequiredCredentialNotFound(CredentialType::X509))
3310        );
3311    }
3312
3313    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
3314    async fn creating_group_with_member_not_supporting_required_extension_type_fails() {
3315        const EXTENSION_TYPE: ExtensionType = ExtensionType::new(33);
3316
3317        let group_creation = make_group_with_required_capabilities(RequiredCapabilitiesExt {
3318            extensions: vec![EXTENSION_TYPE],
3319            ..Default::default()
3320        })
3321        .await
3322        .map(|_| ());
3323
3324        assert_matches!(
3325            group_creation,
3326            Err(MlsError::RequiredExtensionNotFound(EXTENSION_TYPE))
3327        );
3328    }
3329
3330    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
3331    async fn creating_group_with_member_not_supporting_required_proposal_type_fails() {
3332        const PROPOSAL_TYPE: ProposalType = ProposalType::new(33);
3333
3334        let group_creation = make_group_with_required_capabilities(RequiredCapabilitiesExt {
3335            proposals: vec![PROPOSAL_TYPE],
3336            ..Default::default()
3337        })
3338        .await
3339        .map(|_| ());
3340
3341        assert_matches!(
3342            group_creation,
3343            Err(MlsError::RequiredProposalNotFound(PROPOSAL_TYPE))
3344        );
3345    }
3346
3347    #[cfg(feature = "by_ref_proposal")]
3348    #[cfg(not(target_arch = "wasm32"))]
3349    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
3350    async fn creating_group_with_member_not_supporting_external_sender_credential_fails() {
3351        let ext_senders = make_x509_external_senders_ext()
3352            .await
3353            .into_extension()
3354            .unwrap();
3355
3356        let group_creation =
3357            test_client_with_key_pkg(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "alice")
3358                .await
3359                .0
3360                .create_group(
3361                    core::iter::once(ext_senders).collect(),
3362                    Default::default(),
3363                    None,
3364                )
3365                .await
3366                .map(|_| ());
3367
3368        assert_matches!(
3369            group_creation,
3370            Err(MlsError::RequiredCredentialNotFound(CredentialType::X509))
3371        );
3372    }
3373
3374    #[cfg(all(not(target_arch = "wasm32"), feature = "private_message"))]
3375    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
3376    async fn test_group_encrypt_plaintext_padding() {
3377        let protocol_version = TEST_PROTOCOL_VERSION;
3378        // This test requires a cipher suite whose signatures are not variable in length.
3379        let cipher_suite = CipherSuite::CURVE25519_AES128;
3380
3381        let mut test_group = test_group_custom_config(protocol_version, cipher_suite, |b| {
3382            b.mls_rules(
3383                DefaultMlsRules::default()
3384                    .with_encryption_options(EncryptionOptions::new(true, PaddingMode::None)),
3385            )
3386        })
3387        .await;
3388
3389        let without_padding = test_group
3390            .encrypt_application_message(&random_bytes(150), vec![])
3391            .await
3392            .unwrap();
3393
3394        let mut test_group =
3395            test_group_custom_config(protocol_version, cipher_suite, |b| {
3396                b.mls_rules(DefaultMlsRules::default().with_encryption_options(
3397                    EncryptionOptions::new(true, PaddingMode::StepFunction),
3398                ))
3399            })
3400            .await;
3401
3402        let with_step_function_padding = test_group
3403            .encrypt_application_message(&random_bytes(150), vec![])
3404            .await
3405            .unwrap();
3406
3407        assert!(with_step_function_padding.mls_encoded_len() > without_padding.mls_encoded_len());
3408
3409        let mut test_group = test_group_custom_config(protocol_version, cipher_suite, |b| {
3410            b.mls_rules(
3411                DefaultMlsRules::default()
3412                    .with_encryption_options(EncryptionOptions::new(true, PaddingMode::Padme)),
3413            )
3414        })
3415        .await;
3416
3417        let with_padme_padding = test_group
3418            .encrypt_application_message(&random_bytes(150), vec![])
3419            .await
3420            .unwrap();
3421
3422        assert!(with_padme_padding.mls_encoded_len() > without_padding.mls_encoded_len());
3423    }
3424
3425    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
3426    async fn external_commit_requires_external_pub_extension() {
3427        let protocol_version = TEST_PROTOCOL_VERSION;
3428        let cipher_suite = TEST_CIPHER_SUITE;
3429        let group = test_group(protocol_version, cipher_suite).await;
3430
3431        let info = group
3432            .group_info_message(false)
3433            .await
3434            .unwrap()
3435            .into_group_info()
3436            .unwrap();
3437
3438        let info_msg = MlsMessage::new(protocol_version, MlsMessagePayload::GroupInfo(info));
3439
3440        let signing_identity = group.current_member_signing_identity().unwrap().clone();
3441
3442        let res = external_commit::ExternalCommitBuilder::new(
3443            group.group.signer,
3444            signing_identity,
3445            group.group.config,
3446        )
3447        .build(info_msg)
3448        .await
3449        .map(|_| {});
3450
3451        assert_matches!(res, Err(MlsError::MissingExternalPubExtension));
3452    }
3453
3454    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
3455    async fn external_commit_via_commit_options_round_trip() {
3456        let mut group = test_group_custom(
3457            TEST_PROTOCOL_VERSION,
3458            TEST_CIPHER_SUITE,
3459            vec![],
3460            None,
3461            CommitOptions::default()
3462                .with_allow_external_commit(true)
3463                .into(),
3464        )
3465        .await;
3466
3467        let commit_output = group.commit(vec![]).await.unwrap();
3468
3469        let (test_client, _) =
3470            test_client_with_key_pkg(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "bob").await;
3471
3472        test_client
3473            .external_commit_builder()
3474            .unwrap()
3475            .build(commit_output.external_commit_group_info.unwrap())
3476            .await
3477            .unwrap();
3478    }
3479
3480    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
3481    async fn test_path_update_preference() {
3482        let protocol_version = TEST_PROTOCOL_VERSION;
3483        let cipher_suite = TEST_CIPHER_SUITE;
3484
3485        let mut test_group = test_group_custom(
3486            protocol_version,
3487            cipher_suite,
3488            Default::default(),
3489            None,
3490            Some(CommitOptions::new()),
3491        )
3492        .await;
3493
3494        let test_key_package =
3495            test_key_package_message(protocol_version, cipher_suite, "alice").await;
3496
3497        let has_path = test_group
3498            .commit_builder()
3499            .add_member(test_key_package.clone())
3500            .unwrap()
3501            .build()
3502            .await
3503            .unwrap()
3504            .contains_update_path;
3505
3506        assert!(!has_path);
3507
3508        let mut test_group = test_group_custom(
3509            protocol_version,
3510            cipher_suite,
3511            Default::default(),
3512            None,
3513            Some(CommitOptions::new().with_path_required(true)),
3514        )
3515        .await;
3516
3517        let has_path = test_group
3518            .commit_builder()
3519            .add_member(test_key_package)
3520            .unwrap()
3521            .build()
3522            .await
3523            .unwrap()
3524            .contains_update_path;
3525
3526        assert!(has_path)
3527    }
3528
3529    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
3530    async fn test_path_update_preference_override() {
3531        let protocol_version = TEST_PROTOCOL_VERSION;
3532        let cipher_suite = TEST_CIPHER_SUITE;
3533
3534        let mut test_group = test_group_custom(
3535            protocol_version,
3536            cipher_suite,
3537            Default::default(),
3538            None,
3539            Some(CommitOptions::new()),
3540        )
3541        .await;
3542
3543        let has_path = test_group
3544            .commit(vec![])
3545            .await
3546            .unwrap()
3547            .contains_update_path;
3548
3549        assert!(has_path);
3550    }
3551
3552    #[cfg(feature = "private_message")]
3553    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
3554    async fn group_rejects_unencrypted_application_message() {
3555        let protocol_version = TEST_PROTOCOL_VERSION;
3556        let cipher_suite = TEST_CIPHER_SUITE;
3557
3558        let mut alice = test_group(protocol_version, cipher_suite).await;
3559        let (mut bob, _) = alice.join("bob").await;
3560
3561        let message = alice
3562            .make_plaintext(Content::Application(b"hello".to_vec().into()))
3563            .await;
3564
3565        let res = bob.process_incoming_message(message).await;
3566
3567        assert_matches!(res, Err(MlsError::UnencryptedApplicationMessage));
3568    }
3569
3570    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
3571    async fn commit_description_external_commit() {
3572        let mut alice_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
3573
3574        let (bob_identity, secret_key) = get_test_signing_identity(TEST_CIPHER_SUITE, b"bob").await;
3575
3576        let bob = TestClientBuilder::new_for_test()
3577            .signing_identity(bob_identity, secret_key, TEST_CIPHER_SUITE)
3578            .build();
3579
3580        let (bob_group, commit) = bob
3581            .external_commit_builder()
3582            .unwrap()
3583            .build(
3584                alice_group
3585                    .group_info_message_allowing_ext_commit(true)
3586                    .await
3587                    .unwrap(),
3588            )
3589            .await
3590            .unwrap();
3591
3592        let event = alice_group.process_message(commit).await.unwrap();
3593
3594        let ReceivedMessage::Commit(commit_description) = event else {
3595            panic!("expected commit");
3596        };
3597
3598        assert!(commit_description.is_external);
3599        assert_eq!(commit_description.committer, 1);
3600
3601        let new_epoch = match commit_description.effect {
3602            CommitEffect::NewEpoch(new_epoch) => new_epoch,
3603            _ => panic!("unexpected commit effect"),
3604        };
3605
3606        assert_eq!(new_epoch.applied_proposals.len(), 1);
3607
3608        itertools::assert_equal(
3609            bob_group.roster().members_iter(),
3610            alice_group.roster().members_iter(),
3611        );
3612    }
3613
3614    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
3615    async fn can_join_new_group_externally() {
3616        use crate::client::test_utils::TestClientBuilder;
3617
3618        let mut alice_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
3619
3620        let (bob_identity, secret_key) = get_test_signing_identity(TEST_CIPHER_SUITE, b"bob").await;
3621
3622        let bob = TestClientBuilder::new_for_test()
3623            .signing_identity(bob_identity, secret_key, TEST_CIPHER_SUITE)
3624            .build();
3625
3626        let (_, commit) = bob
3627            .external_commit_builder()
3628            .unwrap()
3629            .with_tree_data(alice_group.export_tree().into_owned())
3630            .build(
3631                alice_group
3632                    .group_info_message_allowing_ext_commit(false)
3633                    .await
3634                    .unwrap(),
3635            )
3636            .await
3637            .unwrap();
3638
3639        alice_group.process_message(commit).await.unwrap();
3640    }
3641
3642    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
3643    async fn test_membership_tag_from_non_member() {
3644        let (mut alice_group, mut bob_group) =
3645            test_two_member_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, true).await;
3646
3647        let mut commit_output = alice_group.commit(vec![]).await.unwrap();
3648
3649        let plaintext = match commit_output.commit_message.payload {
3650            MlsMessagePayload::Plain(ref mut plain) => plain,
3651            _ => panic!("Non plaintext message"),
3652        };
3653
3654        plaintext.content.sender = Sender::NewMemberCommit;
3655
3656        let res = bob_group
3657            .process_message(commit_output.commit_message)
3658            .await;
3659
3660        assert_matches!(res, Err(MlsError::MembershipTagForNonMember));
3661    }
3662
3663    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
3664    async fn test_partial_commits() {
3665        let protocol_version = TEST_PROTOCOL_VERSION;
3666        let cipher_suite = TEST_CIPHER_SUITE;
3667
3668        let mut alice = test_group(protocol_version, cipher_suite).await;
3669        let (mut bob, _) = alice.join("bob").await;
3670        let (mut charlie, commit) = alice.join("charlie").await;
3671        bob.process_message(commit).await.unwrap();
3672
3673        let (_, commit) = charlie.join("dave").await;
3674
3675        alice.process_message(commit.clone()).await.unwrap();
3676        bob.process_message(commit.clone()).await.unwrap();
3677
3678        let Content::Commit(commit) = commit.into_plaintext().unwrap().content.content else {
3679            panic!("Expected commit")
3680        };
3681
3682        assert!(commit.path.is_none());
3683    }
3684
3685    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
3686    async fn group_with_path_required() -> TestGroup {
3687        let mut alice = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
3688
3689        alice.config.0.mls_rules.commit_options.path_required = true;
3690
3691        alice
3692    }
3693
3694    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
3695    async fn old_hpke_secrets_are_removed() {
3696        let mut alice = group_with_path_required().await;
3697        alice.join("bob").await;
3698        alice.join("charlie").await;
3699
3700        alice
3701            .commit_builder()
3702            .remove_member(1)
3703            .unwrap()
3704            .build()
3705            .await
3706            .unwrap();
3707
3708        assert!(alice.private_tree.secret_keys[1].is_some());
3709        alice.process_pending_commit().await.unwrap();
3710        assert!(alice.private_tree.secret_keys[1].is_none());
3711    }
3712
3713    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
3714    async fn old_hpke_secrets_of_removed_are_removed() {
3715        let mut alice = group_with_path_required().await;
3716        alice.join("bob").await;
3717        let (mut charlie, _) = alice.join("charlie").await;
3718
3719        let commit = charlie
3720            .commit_builder()
3721            .remove_member(1)
3722            .unwrap()
3723            .build()
3724            .await
3725            .unwrap();
3726
3727        assert!(alice.private_tree.secret_keys[1].is_some());
3728        alice.process_message(commit.commit_message).await.unwrap();
3729        assert!(alice.private_tree.secret_keys[1].is_none());
3730    }
3731
3732    #[cfg(feature = "by_ref_proposal")]
3733    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
3734    async fn old_hpke_secrets_of_updated_are_removed() {
3735        let mut alice = group_with_path_required().await;
3736        let (mut bob, _) = alice.join("bob").await;
3737        let (mut charlie, commit) = alice.join("charlie").await;
3738        bob.process_message(commit).await.unwrap();
3739
3740        let update = bob.propose_update(vec![]).await.unwrap();
3741        charlie.process_message(update.clone()).await.unwrap();
3742        alice.process_message(update).await.unwrap();
3743
3744        let commit = charlie.commit(vec![]).await.unwrap();
3745
3746        assert!(alice.private_tree.secret_keys[1].is_some());
3747        alice.process_message(commit.commit_message).await.unwrap();
3748        assert!(alice.private_tree.secret_keys[1].is_none());
3749    }
3750
3751    #[cfg(feature = "psk")]
3752    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
3753    async fn only_selected_members_of_the_original_group_can_join_subgroup() {
3754        let mut alice = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
3755        let (mut bob, _) = alice.join("bob").await;
3756        let (carol, commit) = alice.join("carol").await;
3757
3758        // Apply the commit that adds carol
3759        bob.process_incoming_message(commit).await.unwrap();
3760
3761        let bob_identity = bob.current_member_signing_identity().unwrap().clone();
3762        let signer = bob.signer.clone();
3763
3764        let new_key_pkg = Client::new(
3765            bob.config.clone(),
3766            Some(signer),
3767            Some((bob_identity, TEST_CIPHER_SUITE)),
3768            TEST_PROTOCOL_VERSION,
3769        )
3770        .generate_key_package_message(Default::default(), Default::default(), None)
3771        .await
3772        .unwrap();
3773
3774        let (mut alice_sub_group, welcome) = alice
3775            .branch(b"subgroup".to_vec(), vec![new_key_pkg], None)
3776            .await
3777            .unwrap();
3778
3779        let welcome = &welcome[0];
3780
3781        let (mut bob_sub_group, _) = bob.join_subgroup(welcome, None, None).await.unwrap();
3782
3783        // Carol can't join
3784        let res = carol.join_subgroup(welcome, None, None).await.map(|_| ());
3785        assert_matches!(res, Err(_));
3786
3787        // Alice and Bob can still talk
3788        let commit_output = alice_sub_group.commit(vec![]).await.unwrap();
3789
3790        bob_sub_group
3791            .process_incoming_message(commit_output.commit_message)
3792            .await
3793            .unwrap();
3794    }
3795
3796    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
3797    async fn joining_group_fails_if_unsupported<F>(
3798        f: F,
3799    ) -> Result<(TestGroup, MlsMessage), MlsError>
3800    where
3801        F: FnMut(&mut TestClientConfig),
3802    {
3803        let mut alice_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
3804        alice_group.join_with_custom_config("alice", false, f).await
3805    }
3806
3807    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
3808    async fn joining_group_fails_if_protocol_version_is_not_supported() {
3809        let res = joining_group_fails_if_unsupported(|config| {
3810            config.0.settings.protocol_versions.clear();
3811        })
3812        .await
3813        .map(|_| ());
3814
3815        assert_matches!(
3816            res,
3817            Err(MlsError::UnsupportedProtocolVersion(v)) if v ==
3818                TEST_PROTOCOL_VERSION
3819        );
3820    }
3821
3822    // WebCrypto does not support disabling ciphersuites
3823    #[cfg(not(target_arch = "wasm32"))]
3824    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
3825    async fn joining_group_fails_if_cipher_suite_is_not_supported() {
3826        let res = joining_group_fails_if_unsupported(|config| {
3827            config
3828                .0
3829                .crypto_provider
3830                .enabled_cipher_suites
3831                .retain(|&x| x != TEST_CIPHER_SUITE);
3832        })
3833        .await
3834        .map(|_| ());
3835
3836        assert_matches!(
3837            res,
3838            Err(MlsError::UnsupportedCipherSuite(TEST_CIPHER_SUITE))
3839        );
3840    }
3841
3842    #[cfg(feature = "private_message")]
3843    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
3844    async fn member_can_see_sender_creds() {
3845        let mut alice_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
3846        let (mut bob_group, _) = alice_group.join("bob").await;
3847
3848        let bob_msg = b"I'm Bob";
3849
3850        let msg = bob_group
3851            .encrypt_application_message(bob_msg, vec![])
3852            .await
3853            .unwrap();
3854
3855        let received_by_alice = alice_group.process_incoming_message(msg).await.unwrap();
3856
3857        assert_matches!(
3858            received_by_alice,
3859            ReceivedMessage::ApplicationMessage(ApplicationMessageDescription { sender_index, .. })
3860                if sender_index == bob_group.current_member_index()
3861        );
3862    }
3863
3864    #[cfg(all(
3865        feature = "export_key_generation",
3866        feature = "private_message",
3867        feature = "secret_tree_access",
3868    ))]
3869    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
3870    async fn export_and_verify_key_generation() {
3871        let mut alice_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
3872        let (mut bob_group, _) = alice_group.join("bob").await;
3873
3874        for i in 0..10 {
3875            let key_gen = bob_group.peek_next_key_generation().unwrap();
3876            assert_eq!(key_gen, i);
3877
3878            let authn_key_gen = key_gen.to_be_bytes();
3879            let msg = bob_group
3880                .encrypt_application_message(&authn_key_gen, vec![])
3881                .await
3882                .unwrap();
3883
3884            let received_by_alice = alice_group.process_incoming_message(msg).await.unwrap();
3885            assert_matches!(
3886                received_by_alice,
3887                ReceivedMessage::ApplicationMessage(ApplicationMessageDescription { unauthenticated_key_generation, .. })
3888                    if unauthenticated_key_generation.unwrap().to_be_bytes() == authn_key_gen
3889            );
3890        }
3891    }
3892
3893    #[cfg(all(feature = "export_key_generation", feature = "private_message",))]
3894    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
3895    async fn verify_key_generation() {
3896        let mut alice_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
3897        let (mut bob_group, _) = alice_group.join("bob").await;
3898
3899        for i in 0u32..10u32 {
3900            let bob_msg = i.to_be_bytes();
3901            let msg = bob_group
3902                .encrypt_application_message(&bob_msg, vec![])
3903                .await
3904                .unwrap();
3905
3906            let received_by_alice = alice_group.process_incoming_message(msg).await.unwrap();
3907            assert_matches!(
3908                received_by_alice,
3909                ReceivedMessage::ApplicationMessage(ApplicationMessageDescription { unauthenticated_key_generation, .. })
3910                    if unauthenticated_key_generation.unwrap().to_be_bytes() == bob_msg
3911            );
3912        }
3913    }
3914
3915    #[cfg(all(
3916        feature = "export_key_generation",
3917        feature = "private_message",
3918        feature = "prior_epoch",
3919        feature = "secret_tree_access"
3920    ))]
3921    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
3922    async fn verify_key_generation_from_prior_epoch() {
3923        let mut alice_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
3924        let (mut bob_group, _) = alice_group.join("bob").await;
3925
3926        let key_gen = bob_group.peek_next_key_generation().unwrap();
3927
3928        let alice_key_gen = alice_group.peek_next_key_generation().unwrap();
3929        assert!(alice_key_gen == key_gen);
3930
3931        let authn_key_gen = key_gen.to_be_bytes();
3932        let msg = bob_group
3933            .encrypt_application_message(&authn_key_gen, vec![])
3934            .await
3935            .unwrap();
3936
3937        alice_group
3938            .encrypt_application_message(&[1, 2, 3], vec![])
3939            .await
3940            .unwrap();
3941
3942        // Advance the key generation so Alice has a different keygen than
3943        // Bob when the message was encrypted.
3944        let alice_key_gen = alice_group.peek_next_key_generation().unwrap();
3945        assert!(alice_key_gen != key_gen);
3946
3947        // Advance the epoch so the message will be decrypted in an epoch
3948        // after it was encrypted.
3949        alice_group.commit(vec![]).await.unwrap();
3950        assert!(alice_group.has_pending_commit());
3951        alice_group.apply_pending_commit().await.unwrap();
3952
3953        let received_by_alice = alice_group.process_incoming_message(msg).await.unwrap();
3954        assert_matches!(
3955            received_by_alice,
3956            ReceivedMessage::ApplicationMessage(ApplicationMessageDescription { unauthenticated_key_generation, .. })
3957                if unauthenticated_key_generation.unwrap().to_be_bytes() == authn_key_gen
3958        );
3959    }
3960
3961    #[cfg(all(
3962        feature = "export_key_generation",
3963        feature = "private_message",
3964        feature = "secret_tree_access",
3965    ))]
3966    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
3967    async fn peek_next_key_generation() {
3968        let mut alice_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
3969        let (mut bob_group, _) = alice_group.join("bob").await;
3970
3971        assert_eq!(bob_group.peek_next_key_generation().unwrap(), 0);
3972        assert_eq!(bob_group.peek_next_key_generation().unwrap(), 0);
3973
3974        let bob_msg = b"I'm Bob";
3975        bob_group
3976            .encrypt_application_message(bob_msg, vec![])
3977            .await
3978            .unwrap();
3979
3980        assert_eq!(bob_group.peek_next_key_generation().unwrap(), 1);
3981        bob_group
3982            .encrypt_application_message(bob_msg, vec![])
3983            .await
3984            .unwrap();
3985
3986        bob_group
3987            .encrypt_application_message(bob_msg, vec![])
3988            .await
3989            .unwrap();
3990
3991        assert_eq!(bob_group.peek_next_key_generation().unwrap(), 3);
3992    }
3993
3994    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
3995    async fn members_of_a_group_have_identical_authentication_secrets() {
3996        let mut alice_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
3997        let (bob_group, _) = alice_group.join("bob").await;
3998
3999        assert_eq!(
4000            alice_group.epoch_authenticator().unwrap(),
4001            bob_group.epoch_authenticator().unwrap()
4002        );
4003    }
4004
4005    #[cfg(feature = "private_message")]
4006    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
4007    async fn member_cannot_decrypt_same_message_twice() {
4008        let mut alice_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
4009        let (mut bob_group, _) = alice_group.join("bob").await;
4010
4011        let message = alice_group
4012            .encrypt_application_message(b"foobar", Vec::new())
4013            .await
4014            .unwrap();
4015
4016        let received_message = bob_group
4017            .process_incoming_message(message.clone())
4018            .await
4019            .unwrap();
4020
4021        assert_matches!(
4022            received_message,
4023            ReceivedMessage::ApplicationMessage(m) if m.data() == b"foobar"
4024        );
4025
4026        let res = bob_group.process_incoming_message(message).await;
4027
4028        assert_matches!(res, Err(MlsError::KeyMissing(0)));
4029    }
4030
4031    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
4032    async fn removing_requirements_allows_to_add() {
4033        let mut alice_group = test_group_custom(
4034            TEST_PROTOCOL_VERSION,
4035            TEST_CIPHER_SUITE,
4036            vec![17.into()],
4037            None,
4038            None,
4039        )
4040        .await;
4041
4042        alice_group
4043            .commit_builder()
4044            .set_group_context_ext(
4045                vec![RequiredCapabilitiesExt {
4046                    extensions: vec![17.into()],
4047                    ..Default::default()
4048                }
4049                .into_extension()
4050                .unwrap()]
4051                .try_into()
4052                .unwrap(),
4053            )
4054            .unwrap()
4055            .build()
4056            .await
4057            .unwrap();
4058
4059        alice_group.process_pending_commit().await.unwrap();
4060
4061        let test_key_package =
4062            test_key_package(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "bob").await;
4063
4064        let test_key_package = MlsMessage::new(
4065            TEST_PROTOCOL_VERSION,
4066            MlsMessagePayload::KeyPackage(test_key_package),
4067        );
4068
4069        alice_group
4070            .commit_builder()
4071            .add_member(test_key_package.clone())
4072            .unwrap()
4073            .set_group_context_ext(Default::default())
4074            .unwrap()
4075            .build()
4076            .await
4077            .unwrap();
4078
4079        let CommitEffect::NewEpoch(new_epoch) =
4080            alice_group.process_pending_commit().await.unwrap().effect
4081        else {
4082            panic!("unexpected commit effect")
4083        };
4084
4085        assert_eq!(new_epoch.applied_proposals.len(), 2);
4086
4087        assert_matches!(
4088            new_epoch.applied_proposals[0],
4089            ProposalInfo { proposal: Proposal::Add(ref add), .. }
4090                if add.key_package == test_key_package.into_key_package().unwrap()
4091        );
4092
4093        assert_eq!(alice_group.roster().members_iter().count(), 2);
4094    }
4095
4096    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
4097    async fn commit_leaf_wrong_source() {
4098        // RFC, 13.4.2. "The leaf_node_source field MUST be set to commit."
4099        let mut groups = test_n_member_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, 3).await;
4100
4101        groups[0].commit_modifiers.modify_leaf = |leaf, sk| {
4102            leaf.leaf_node_source = LeafNodeSource::Update;
4103            Some(sk.clone())
4104        };
4105
4106        let commit_output = groups[0].commit(vec![]).await.unwrap();
4107
4108        let res = groups[2]
4109            .process_message(commit_output.commit_message)
4110            .await;
4111
4112        assert_matches!(res, Err(MlsError::InvalidLeafNodeSource));
4113    }
4114
4115    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
4116    async fn commit_leaf_same_hpke_key() {
4117        // RFC 13.4.2. "Verify that the encryption_key value in the LeafNode is different from the committer's current leaf node"
4118
4119        let mut groups = test_n_member_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, 3).await;
4120
4121        // Group 0 starts using fixed key
4122        groups[0].commit_modifiers.modify_leaf = |leaf, sk| {
4123            leaf.public_key = get_test_25519_key(1u8);
4124            Some(sk.clone())
4125        };
4126
4127        let commit_output = groups[0].commit(vec![]).await.unwrap();
4128        groups[0].process_pending_commit().await.unwrap();
4129        groups[2]
4130            .process_message(commit_output.commit_message)
4131            .await
4132            .unwrap();
4133
4134        // Group 0 tries to use the fixed key againd
4135        let commit_output = groups[0].commit(vec![]).await.unwrap();
4136
4137        let res = groups[2]
4138            .process_message(commit_output.commit_message)
4139            .await;
4140
4141        assert_matches!(res, Err(MlsError::SameHpkeKey(0)));
4142    }
4143
4144    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
4145    async fn commit_leaf_duplicate_hpke_key() {
4146        // RFC 8.3 "Verify that the following fields are unique among the members of the group: `encryption_key`"
4147
4148        if TEST_CIPHER_SUITE != CipherSuite::CURVE25519_AES128
4149            && TEST_CIPHER_SUITE != CipherSuite::CURVE25519_CHACHA
4150        {
4151            return;
4152        }
4153
4154        let mut groups = test_n_member_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, 10).await;
4155
4156        // Group 1 uses the fixed key
4157        groups[1].commit_modifiers.modify_leaf = |leaf, sk| {
4158            leaf.public_key = get_test_25519_key(1u8);
4159            Some(sk.clone())
4160        };
4161
4162        let commit_output = groups.get_mut(1).unwrap().commit(vec![]).await.unwrap();
4163
4164        process_commit(&mut groups, commit_output.commit_message, 1).await;
4165
4166        // Group 0 tries to use the fixed key too
4167        groups[0].commit_modifiers.modify_leaf = |leaf, sk| {
4168            leaf.public_key = get_test_25519_key(1u8);
4169            Some(sk.clone())
4170        };
4171
4172        let commit_output = groups[0].commit(vec![]).await.unwrap();
4173
4174        let res = groups[7]
4175            .process_message(commit_output.commit_message)
4176            .await;
4177
4178        assert_matches!(res, Err(MlsError::DuplicateLeafData(_)));
4179    }
4180
4181    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
4182    async fn commit_leaf_duplicate_signature_key() {
4183        // RFC 8.3 "Verify that the following fields are unique among the members of the group: `signature_key`"
4184
4185        if TEST_CIPHER_SUITE != CipherSuite::CURVE25519_AES128
4186            && TEST_CIPHER_SUITE != CipherSuite::CURVE25519_CHACHA
4187        {
4188            return;
4189        }
4190
4191        let mut groups = test_n_member_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, 10).await;
4192
4193        // Group 1 uses the fixed key
4194        groups[1].commit_modifiers.modify_leaf = |leaf, _| {
4195            let sk = hex!(
4196                "3468b4c890255c983e3d5cbf5cb64c1ef7f6433a518f2f3151d6672f839a06ebcad4fc381fe61822af45135c82921a348e6f46643d66ddefc70483565433714b"
4197            )
4198            .into();
4199
4200            leaf.signing_identity.signature_key =
4201                hex!("cad4fc381fe61822af45135c82921a348e6f46643d66ddefc70483565433714b").into();
4202
4203            Some(sk)
4204        };
4205
4206        let commit_output = groups.get_mut(1).unwrap().commit(vec![]).await.unwrap();
4207
4208        process_commit(&mut groups, commit_output.commit_message, 1).await;
4209
4210        // Group 0 tries to use the fixed key too
4211        groups[0].commit_modifiers.modify_leaf = |leaf, _| {
4212            let sk = hex!(
4213                "3468b4c890255c983e3d5cbf5cb64c1ef7f6433a518f2f3151d6672f839a06ebcad4fc381fe61822af45135c82921a348e6f46643d66ddefc70483565433714b"
4214            )
4215            .into();
4216
4217            leaf.signing_identity.signature_key =
4218                hex!("cad4fc381fe61822af45135c82921a348e6f46643d66ddefc70483565433714b").into();
4219
4220            Some(sk)
4221        };
4222
4223        let commit_output = groups[0].commit(vec![]).await.unwrap();
4224
4225        let res = groups[7]
4226            .process_message(commit_output.commit_message)
4227            .await;
4228
4229        assert_matches!(res, Err(MlsError::DuplicateLeafData(_)));
4230    }
4231
4232    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
4233    async fn commit_leaf_incorrect_signature() {
4234        let mut groups = test_n_member_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, 3).await;
4235
4236        groups[0].commit_modifiers.modify_leaf = |leaf, _| {
4237            leaf.signature[0] ^= 1;
4238            None
4239        };
4240
4241        let commit_output = groups[0].commit(vec![]).await.unwrap();
4242
4243        let res = groups[2]
4244            .process_message(commit_output.commit_message)
4245            .await;
4246
4247        assert_matches!(res, Err(MlsError::InvalidSignature));
4248    }
4249
4250    #[cfg(not(target_arch = "wasm32"))]
4251    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
4252    async fn commit_leaf_not_supporting_used_context_extension() {
4253        const EXT_TYPE: ExtensionType = ExtensionType::new(999);
4254
4255        // The new leaf of the committer doesn't support an extension set in group context
4256        let extension = Extension::new(EXT_TYPE, vec![]);
4257
4258        let mut groups =
4259            get_test_groups_with_features(3, vec![extension].into(), Default::default()).await;
4260
4261        groups[0].commit_modifiers.modify_leaf = |leaf, sk| {
4262            leaf.capabilities = get_test_capabilities();
4263            Some(sk.clone())
4264        };
4265
4266        let commit_output = groups[0].commit(vec![]).await.unwrap();
4267
4268        let res = groups[1]
4269            .process_incoming_message(commit_output.commit_message)
4270            .await;
4271
4272        assert_matches!(res, Err(MlsError::UnsupportedGroupExtension(EXT_TYPE)));
4273    }
4274
4275    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
4276    async fn commit_leaf_not_supporting_required_extension() {
4277        // The new leaf of the committer doesn't support an extension required by group context
4278
4279        let extension = RequiredCapabilitiesExt {
4280            extensions: vec![999.into()],
4281            proposals: vec![],
4282            credentials: vec![],
4283        };
4284
4285        let extensions = vec![extension.into_extension().unwrap()];
4286        let mut groups =
4287            get_test_groups_with_features(3, extensions.into(), Default::default()).await;
4288
4289        groups[0].commit_modifiers.modify_leaf = |leaf, sk| {
4290            leaf.capabilities = Capabilities::default();
4291            Some(sk.clone())
4292        };
4293
4294        let commit_output = groups[0].commit(vec![]).await.unwrap();
4295
4296        let res = groups[2]
4297            .process_incoming_message(commit_output.commit_message)
4298            .await;
4299
4300        assert!(res.is_err());
4301    }
4302
4303    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
4304    async fn commit_leaf_has_unsupported_credential() {
4305        // The new leaf of the committer has a credential unsupported by another leaf
4306        let mut groups =
4307            get_test_groups_with_features(3, Default::default(), Default::default()).await;
4308
4309        for group in groups.iter_mut() {
4310            group.config.0.identity_provider.allow_any_custom = true;
4311        }
4312
4313        groups[0].commit_modifiers.modify_leaf = |leaf, sk| {
4314            leaf.signing_identity.credential = Credential::Custom(CustomCredential::new(
4315                CredentialType::new(43),
4316                leaf.signing_identity
4317                    .credential
4318                    .as_basic()
4319                    .unwrap()
4320                    .identifier
4321                    .to_vec(),
4322            ));
4323
4324            Some(sk.clone())
4325        };
4326
4327        let commit_output = groups[0].commit(vec![]).await.unwrap();
4328
4329        let res = groups[2]
4330            .process_incoming_message(commit_output.commit_message)
4331            .await;
4332
4333        assert_matches!(res, Err(MlsError::CredentialTypeOfNewLeafIsUnsupported));
4334    }
4335
4336    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
4337    async fn commit_leaf_not_supporting_credential_used_in_another_leaf() {
4338        // The new leaf of the committer doesn't support another leaf's credential
4339
4340        let mut groups =
4341            get_test_groups_with_features(3, Default::default(), Default::default()).await;
4342
4343        groups[0].commit_modifiers.modify_leaf = |leaf, sk| {
4344            leaf.capabilities.credentials = vec![2.into()];
4345            Some(sk.clone())
4346        };
4347
4348        let commit_output = groups[0].commit(vec![]).await.unwrap();
4349
4350        let res = groups[2]
4351            .process_incoming_message(commit_output.commit_message)
4352            .await;
4353
4354        assert_matches!(res, Err(MlsError::InUseCredentialTypeUnsupportedByNewLeaf));
4355    }
4356
4357    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
4358    async fn commit_leaf_not_supporting_required_credential() {
4359        // The new leaf of the committer doesn't support a credential required by group context
4360
4361        let extension = RequiredCapabilitiesExt {
4362            extensions: vec![],
4363            proposals: vec![],
4364            credentials: vec![1.into()],
4365        };
4366
4367        let extensions = vec![extension.into_extension().unwrap()];
4368        let mut groups =
4369            get_test_groups_with_features(3, extensions.into(), Default::default()).await;
4370
4371        groups[0].commit_modifiers.modify_leaf = |leaf, sk| {
4372            leaf.capabilities.credentials = vec![2.into()];
4373            Some(sk.clone())
4374        };
4375
4376        let commit_output = groups[0].commit(vec![]).await.unwrap();
4377
4378        let res = groups[2]
4379            .process_incoming_message(commit_output.commit_message)
4380            .await;
4381
4382        assert_matches!(res, Err(MlsError::RequiredCredentialNotFound(_)));
4383    }
4384
4385    #[cfg(feature = "by_ref_proposal")]
4386    #[cfg(not(target_arch = "wasm32"))]
4387    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
4388    async fn make_x509_external_senders_ext() -> ExternalSendersExt {
4389        let (_, ext_sender_pk) = test_cipher_suite_provider(TEST_CIPHER_SUITE)
4390            .signature_key_generate()
4391            .await
4392            .unwrap();
4393
4394        let ext_sender_id = SigningIdentity {
4395            signature_key: ext_sender_pk,
4396            credential: Credential::X509(CertificateChain::from(vec![random_bytes(32)])),
4397        };
4398
4399        ExternalSendersExt::new(vec![ext_sender_id])
4400    }
4401
4402    #[cfg(feature = "by_ref_proposal")]
4403    #[cfg(not(target_arch = "wasm32"))]
4404    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
4405    async fn commit_leaf_not_supporting_external_sender_credential_leads_to_rejected_commit() {
4406        let ext_senders = make_x509_external_senders_ext()
4407            .await
4408            .into_extension()
4409            .unwrap();
4410
4411        let mut alice = ClientBuilder::new()
4412            .crypto_provider(TestCryptoProvider::new())
4413            .identity_provider(
4414                BasicWithCustomProvider::default().with_credential_type(CredentialType::X509),
4415            )
4416            .with_random_signing_identity("alice", TEST_CIPHER_SUITE)
4417            .await
4418            .build()
4419            .create_group(vec![ext_senders].into(), Default::default(), None)
4420            .await
4421            .unwrap();
4422
4423        let bob = ClientBuilder::new()
4424            .crypto_provider(TestCryptoProvider::new())
4425            .identity_provider(
4426                BasicWithCustomProvider::default().with_credential_type(CredentialType::X509),
4427            )
4428            .with_random_signing_identity("bob", TEST_CIPHER_SUITE)
4429            .await
4430            .build();
4431
4432        let kp = bob
4433            .generate_key_package_message(Default::default(), Default::default(), None)
4434            .await
4435            .unwrap();
4436
4437        let commit = alice
4438            .commit_builder()
4439            .add_member(kp)
4440            .unwrap()
4441            .build()
4442            .await
4443            .unwrap();
4444
4445        let (mut bob, _) = bob
4446            .join_group(None, &commit.welcome_messages[0], None)
4447            .await
4448            .unwrap();
4449
4450        alice.apply_pending_commit().await.unwrap();
4451
4452        // New leaf supports only basic credentials (used by the group) but not X509 used by external sender
4453        alice.commit_modifiers.modify_leaf = |leaf, sk| {
4454            leaf.capabilities.credentials = vec![CredentialType::BASIC];
4455            Some(sk.clone())
4456        };
4457
4458        let commit = alice.commit(vec![]).await.unwrap();
4459        let res = bob.process_incoming_message(commit.commit_message).await;
4460
4461        assert_matches!(
4462            res,
4463            Err(MlsError::RequiredCredentialNotFound(CredentialType::X509))
4464        );
4465    }
4466
4467    #[cfg(feature = "by_ref_proposal")]
4468    #[cfg(not(target_arch = "wasm32"))]
4469    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
4470    async fn node_not_supporting_external_sender_credential_cannot_join_group() {
4471        let ext_senders = make_x509_external_senders_ext()
4472            .await
4473            .into_extension()
4474            .unwrap();
4475
4476        let mut alice = ClientBuilder::new()
4477            .crypto_provider(TestCryptoProvider::new())
4478            .identity_provider(
4479                BasicWithCustomProvider::default().with_credential_type(CredentialType::X509),
4480            )
4481            .with_random_signing_identity("alice", TEST_CIPHER_SUITE)
4482            .await
4483            .build()
4484            .create_group(
4485                core::iter::once(ext_senders).collect(),
4486                Default::default(),
4487                None,
4488            )
4489            .await
4490            .unwrap();
4491
4492        let (_, bob_key_pkg) =
4493            test_client_with_key_pkg(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "bob").await;
4494
4495        let commit = alice
4496            .commit_builder()
4497            .add_member(bob_key_pkg)
4498            .unwrap()
4499            .build()
4500            .await;
4501
4502        assert_matches!(
4503            commit,
4504            Err(MlsError::RequiredCredentialNotFound(CredentialType::X509))
4505        );
4506    }
4507
4508    #[cfg(feature = "by_ref_proposal")]
4509    #[cfg(not(target_arch = "wasm32"))]
4510    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
4511    async fn external_senders_extension_is_rejected_if_member_does_not_support_credential_type() {
4512        let mut alice = ClientBuilder::new()
4513            .crypto_provider(TestCryptoProvider::new())
4514            .identity_provider(
4515                BasicWithCustomProvider::default().with_credential_type(CredentialType::X509),
4516            )
4517            .with_random_signing_identity("alice", TEST_CIPHER_SUITE)
4518            .await
4519            .build()
4520            .create_group(Default::default(), Default::default(), None)
4521            .await
4522            .unwrap();
4523
4524        let (_, bob_key_pkg) =
4525            test_client_with_key_pkg(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "bob").await;
4526
4527        alice
4528            .commit_builder()
4529            .add_member(bob_key_pkg)
4530            .unwrap()
4531            .build()
4532            .await
4533            .unwrap();
4534
4535        alice.apply_pending_commit().await.unwrap();
4536        assert_eq!(alice.roster().members_iter().count(), 2);
4537
4538        let ext_senders = make_x509_external_senders_ext()
4539            .await
4540            .into_extension()
4541            .unwrap();
4542
4543        let res = alice
4544            .commit_builder()
4545            .set_group_context_ext(core::iter::once(ext_senders).collect())
4546            .unwrap()
4547            .build()
4548            .await;
4549
4550        assert_matches!(
4551            res,
4552            Err(MlsError::RequiredCredentialNotFound(CredentialType::X509))
4553        );
4554    }
4555
4556    /*
4557     * Edge case paths
4558     */
4559
4560    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
4561    async fn committing_degenerate_path_succeeds() {
4562        let mut groups = test_n_member_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, 10).await;
4563
4564        groups[0].commit_modifiers.modify_tree = |tree: &mut TreeKemPublic| {
4565            tree.update_node(get_test_25519_key(1u8), 1).unwrap();
4566            tree.update_node(get_test_25519_key(1u8), 3).unwrap();
4567        };
4568
4569        groups[0].commit_modifiers.modify_leaf = |leaf, sk| {
4570            leaf.public_key = get_test_25519_key(1u8);
4571            Some(sk.clone())
4572        };
4573
4574        let commit_output = groups[0].commit(vec![]).await.unwrap();
4575
4576        let res = groups[7]
4577            .process_message(commit_output.commit_message)
4578            .await;
4579
4580        assert!(res.is_ok());
4581    }
4582
4583    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
4584    async fn inserting_key_in_filtered_node_fails() {
4585        let mut groups = test_n_member_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, 10).await;
4586
4587        let commit_output = groups[0]
4588            .commit_builder()
4589            .remove_member(1)
4590            .unwrap()
4591            .build()
4592            .await
4593            .unwrap();
4594
4595        groups[0].process_pending_commit().await.unwrap();
4596
4597        for group in groups.iter_mut().skip(2) {
4598            group
4599                .process_message(commit_output.commit_message.clone())
4600                .await
4601                .unwrap();
4602        }
4603
4604        groups[0].commit_modifiers.modify_tree = |tree: &mut TreeKemPublic| {
4605            tree.update_node(get_test_25519_key(1u8), 1).unwrap();
4606        };
4607
4608        groups[0].commit_modifiers.modify_path = |path: Vec<UpdatePathNode>| {
4609            let mut path = path;
4610            let mut node = path[0].clone();
4611            node.public_key = get_test_25519_key(1u8);
4612            path.insert(0, node);
4613            path
4614        };
4615
4616        let commit_output = groups[0].commit(vec![]).await.unwrap();
4617
4618        let res = groups[7]
4619            .process_message(commit_output.commit_message)
4620            .await;
4621
4622        // We should get a path validation error, since the path is too long
4623        assert_matches!(res, Err(MlsError::WrongPathLen));
4624    }
4625
4626    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
4627    async fn commit_with_too_short_path_fails() {
4628        let mut groups = test_n_member_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, 10).await;
4629
4630        let commit_output = groups[0]
4631            .commit_builder()
4632            .remove_member(1)
4633            .unwrap()
4634            .build()
4635            .await
4636            .unwrap();
4637
4638        groups[0].process_pending_commit().await.unwrap();
4639
4640        for group in groups.iter_mut().skip(2) {
4641            group
4642                .process_message(commit_output.commit_message.clone())
4643                .await
4644                .unwrap();
4645        }
4646
4647        groups[0].commit_modifiers.modify_path = |path: Vec<UpdatePathNode>| {
4648            let mut path = path;
4649            path.pop();
4650            path
4651        };
4652
4653        let commit_output = groups[0].commit(vec![]).await.unwrap();
4654
4655        let res = groups[7]
4656            .process_message(commit_output.commit_message)
4657            .await;
4658
4659        assert!(res.is_err());
4660    }
4661
4662    #[cfg(feature = "by_ref_proposal")]
4663    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
4664    async fn update_proposal_can_change_credential() {
4665        let mut groups = test_n_member_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, 3).await;
4666        let (identity, secret_key) = get_test_signing_identity(TEST_CIPHER_SUITE, b"member").await;
4667
4668        let update = groups[0]
4669            .propose_update_with_identity(secret_key, identity.clone(), vec![])
4670            .await
4671            .unwrap();
4672
4673        groups[1].process_message(update).await.unwrap();
4674        let commit_output = groups[1].commit(vec![]).await.unwrap();
4675
4676        // Check that the credential was updated by in the committer's state.
4677        groups[1].process_pending_commit().await.unwrap();
4678        let new_member = groups[1].roster().member_with_index(0).unwrap();
4679
4680        assert_eq!(
4681            new_member.signing_identity.credential,
4682            get_test_basic_credential(b"member".to_vec())
4683        );
4684
4685        assert_eq!(
4686            new_member.signing_identity.signature_key,
4687            identity.signature_key
4688        );
4689
4690        // Check that the credential was updated in the updater's state.
4691        groups[0]
4692            .process_message(commit_output.commit_message)
4693            .await
4694            .unwrap();
4695        let new_member = groups[0].roster().member_with_index(0).unwrap();
4696
4697        assert_eq!(
4698            new_member.signing_identity.credential,
4699            get_test_basic_credential(b"member".to_vec())
4700        );
4701
4702        assert_eq!(
4703            new_member.signing_identity.signature_key,
4704            identity.signature_key
4705        );
4706    }
4707
4708    #[cfg(feature = "by_ref_proposal")]
4709    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
4710    async fn receiving_commit_with_old_adds_fails() {
4711        let mut groups = test_n_member_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, 2).await;
4712
4713        let key_package =
4714            test_key_package_message(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "foobar").await;
4715
4716        let proposal = groups[0].propose_add(key_package, vec![]).await.unwrap();
4717
4718        let commit = groups[0].commit(vec![]).await.unwrap().commit_message;
4719
4720        // 10 years from now
4721        let future_time = MlsTime::now().seconds_since_epoch() + 10 * 365 * 24 * 3600;
4722
4723        let future_time =
4724            MlsTime::from_duration_since_epoch(core::time::Duration::from_secs(future_time));
4725
4726        groups[1].process_incoming_message(proposal).await.unwrap();
4727        let res = groups[1]
4728            .process_incoming_message_with_time(commit, future_time)
4729            .await;
4730
4731        assert_matches!(
4732            res,
4733            Err(MlsError::InvalidLifetime { timestamp, .. })
4734                if timestamp == future_time
4735        );
4736    }
4737
4738    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
4739    async fn can_process_commit_from_self_with_time() {
4740        let mut alice = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
4741
4742        let commit = alice.commit(Vec::new()).await.unwrap();
4743        assert!(alice.has_pending_commit());
4744
4745        alice
4746            .process_incoming_message_with_time(commit.commit_message, MlsTime::now())
4747            .await
4748            .unwrap();
4749    }
4750
4751    #[cfg(feature = "custom_proposal")]
4752    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
4753    async fn custom_proposal_setup() -> (TestGroup, TestGroup) {
4754        let mut alice = test_group_custom_config(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, |b| {
4755            b.custom_proposal_type(TEST_CUSTOM_PROPOSAL_TYPE)
4756        })
4757        .await;
4758
4759        let (bob, _) = alice
4760            .join_with_custom_config("bob", true, |c| {
4761                c.0.settings
4762                    .custom_proposal_types
4763                    .push(TEST_CUSTOM_PROPOSAL_TYPE)
4764            })
4765            .await
4766            .unwrap();
4767
4768        (alice, bob)
4769    }
4770
4771    #[cfg(all(
4772        feature = "by_ref_proposal",
4773        feature = "custom_proposal",
4774        feature = "self_remove_proposal"
4775    ))]
4776    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
4777    async fn self_remove_group_setup() -> (TestGroup, TestGroup) {
4778        let mut alice = test_group_custom_config(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, |b| {
4779            b.custom_proposal_type(ProposalType::SELF_REMOVE)
4780        })
4781        .await;
4782
4783        let (bob, _) = alice
4784            .join_with_custom_config("bob", true, |c| {
4785                c.0.settings
4786                    .custom_proposal_types
4787                    .push(ProposalType::SELF_REMOVE)
4788            })
4789            .await
4790            .unwrap();
4791
4792        (alice, bob)
4793    }
4794
4795    #[cfg(feature = "custom_proposal")]
4796    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
4797    async fn custom_proposal_by_value() {
4798        let (mut alice, mut bob) = custom_proposal_setup().await;
4799
4800        let custom_proposal = CustomProposal::new(TEST_CUSTOM_PROPOSAL_TYPE, vec![0, 1, 2]);
4801
4802        let commit = alice
4803            .commit_builder()
4804            .custom_proposal(custom_proposal.clone())
4805            .build()
4806            .await
4807            .unwrap()
4808            .commit_message;
4809
4810        let ReceivedMessage::Commit(CommitMessageDescription {
4811            effect: CommitEffect::NewEpoch(new_epoch),
4812            ..
4813        }) = bob.process_incoming_message(commit).await.unwrap()
4814        else {
4815            panic!("unexpected commit effect");
4816        };
4817
4818        assert_eq!(new_epoch.applied_proposals.len(), 1);
4819
4820        assert_eq!(
4821            new_epoch.applied_proposals[0].proposal,
4822            Proposal::Custom(custom_proposal)
4823        );
4824    }
4825
4826    #[cfg(feature = "custom_proposal")]
4827    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
4828    async fn custom_proposal_by_reference() {
4829        let (mut alice, mut bob) = custom_proposal_setup().await;
4830
4831        let custom_proposal = CustomProposal::new(TEST_CUSTOM_PROPOSAL_TYPE, vec![0, 1, 2]);
4832
4833        let proposal = alice
4834            .propose_custom(custom_proposal.clone(), vec![])
4835            .await
4836            .unwrap();
4837
4838        let recv_prop = bob.process_incoming_message(proposal).await.unwrap();
4839
4840        assert_matches!(recv_prop, ReceivedMessage::Proposal(ProposalMessageDescription { proposal: Proposal::Custom(c), ..})
4841            if c == custom_proposal);
4842
4843        let commit = bob.commit(vec![]).await.unwrap().commit_message;
4844
4845        let ReceivedMessage::Commit(CommitMessageDescription {
4846            effect: CommitEffect::NewEpoch(new_epoch),
4847            ..
4848        }) = bob.process_incoming_message(commit).await.unwrap()
4849        else {
4850            panic!("unexpected commit effect");
4851        };
4852
4853        assert_eq!(new_epoch.applied_proposals.len(), 1);
4854
4855        assert_eq!(
4856            new_epoch.applied_proposals[0].proposal,
4857            Proposal::Custom(custom_proposal)
4858        );
4859    }
4860
4861    #[cfg(all(feature = "custom_proposal", feature = "gsma_rcs_e2ee_feature"))]
4862    #[derive(MlsSize, MlsDecode, MlsEncode, Debug, PartialEq)]
4863    struct RcsSignature {}
4864    #[cfg(all(feature = "custom_proposal", feature = "gsma_rcs_e2ee_feature"))]
4865    impl MlsCustomProposal for RcsSignature {
4866        fn proposal_type() -> ProposalType {
4867            ProposalType::RCS_SIGNATURE
4868        }
4869
4870        fn to_custom_proposal(&self) -> Result<CustomProposal, mls_rs_codec::Error> {
4871            Ok(CustomProposal::new(Self::proposal_type(), Vec::new()))
4872        }
4873
4874        fn from_custom_proposal(proposal: &CustomProposal) -> Result<Self, mls_rs_codec::Error> {
4875            if proposal.proposal_type() != Self::proposal_type() {
4876                return Err(mls_rs_codec::Error::Custom(4));
4877            }
4878
4879            Ok(Self {})
4880        }
4881    }
4882
4883    #[cfg(all(feature = "custom_proposal", feature = "gsma_rcs_e2ee_feature"))]
4884    #[derive(MlsSize, MlsDecode, MlsEncode, Debug, PartialEq)]
4885    struct RcsServerRemove {
4886        to_remove: u32,
4887    }
4888
4889    #[cfg(all(feature = "custom_proposal", feature = "gsma_rcs_e2ee_feature"))]
4890    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
4891    async fn custom_proposal_custom_data_encoding_rcs_signature() {
4892        let mut alice = test_group_custom_config(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, |b| {
4893            b.custom_proposal_type(ProposalType::RCS_SIGNATURE)
4894        })
4895        .await;
4896
4897        let (mut bob, _) = alice
4898            .join_with_custom_config("bob", true, |c| {
4899                c.0.settings
4900                    .custom_proposal_types
4901                    .push(ProposalType::RCS_SIGNATURE)
4902            })
4903            .await
4904            .unwrap();
4905
4906        let custom_proposal = RcsSignature {};
4907
4908        let proposal = alice
4909            .propose_custom(custom_proposal.to_custom_proposal().unwrap(), vec![])
4910            .await
4911            .unwrap();
4912
4913        let recv_prop = bob.process_incoming_message(proposal).await.unwrap();
4914
4915        assert_matches!(recv_prop, ReceivedMessage::Proposal(ProposalMessageDescription { proposal: Proposal::Custom(c), ..})
4916            if c == custom_proposal.to_custom_proposal().unwrap());
4917
4918        let commit = bob.commit(vec![]).await.unwrap().commit_message;
4919
4920        let ReceivedMessage::Commit(CommitMessageDescription {
4921            effect: CommitEffect::NewEpoch(new_epoch),
4922            ..
4923        }) = bob.process_incoming_message(commit).await.unwrap()
4924        else {
4925            panic!("unexpected commit effect");
4926        };
4927
4928        assert_eq!(new_epoch.applied_proposals.len(), 1);
4929
4930        let Proposal::Custom(ref c) = new_epoch.applied_proposals[0].proposal else {
4931            panic!("unexpected non-custom proposal");
4932        };
4933        assert_eq!(
4934            RcsSignature::from_custom_proposal(c).unwrap(),
4935            custom_proposal
4936        );
4937    }
4938
4939    #[cfg(all(feature = "custom_proposal", feature = "gsma_rcs_e2ee_feature"))]
4940    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
4941    async fn custom_proposal_custom_data_encoding_rcs_server_remove() {
4942        let mut alice = test_group_custom_config(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, |b| {
4943            b.custom_proposal_type(ProposalType::RCS_SERVER_REMOVE)
4944        })
4945        .await;
4946
4947        let (mut bob, _) = alice
4948            .join_with_custom_config("bob", true, |c| {
4949                c.0.settings
4950                    .custom_proposal_types
4951                    .push(ProposalType::RCS_SERVER_REMOVE)
4952            })
4953            .await
4954            .unwrap();
4955
4956        let server_remove_proposal = RcsServerRemove { to_remove: 1 };
4957        // show directly how the encoding works
4958        let custom_proposal = CustomProposal::new(
4959            ProposalType::RCS_SERVER_REMOVE,
4960            server_remove_proposal.mls_encode_to_vec().unwrap(),
4961        );
4962
4963        let proposal = alice
4964            .propose_custom(custom_proposal.clone(), vec![])
4965            .await
4966            .unwrap();
4967
4968        let recv_prop = bob.process_incoming_message(proposal).await.unwrap();
4969
4970        assert_matches!(recv_prop, ReceivedMessage::Proposal(ProposalMessageDescription { proposal: Proposal::Custom(c), ..})
4971            if c == custom_proposal);
4972
4973        let commit = bob.commit(vec![]).await.unwrap().commit_message;
4974
4975        let ReceivedMessage::Commit(CommitMessageDescription {
4976            effect: CommitEffect::NewEpoch(new_epoch),
4977            ..
4978        }) = bob.process_incoming_message(commit).await.unwrap()
4979        else {
4980            panic!("unexpected commit effect");
4981        };
4982
4983        assert_eq!(new_epoch.applied_proposals.len(), 1);
4984
4985        let Proposal::Custom(ref c) = new_epoch.applied_proposals[0].proposal else {
4986            panic!("unexpected non-custom proposal");
4987        };
4988        let reader = c.data().to_vec();
4989        let server_remove_decoded = RcsServerRemove::mls_decode(&mut reader.as_slice()).unwrap();
4990        assert_eq!(server_remove_decoded, server_remove_proposal);
4991    }
4992
4993    #[cfg(feature = "psk")]
4994    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
4995    async fn can_join_with_psk() {
4996        let mut alice = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
4997
4998        let (bob, key_pkg) =
4999            test_client_with_key_pkg(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "bob").await;
5000
5001        let psk_id = ExternalPskId::new(vec![0]);
5002        let psk = PreSharedKey::from(vec![0]);
5003
5004        alice
5005            .config
5006            .secret_store()
5007            .insert(psk_id.clone(), psk.clone());
5008
5009        bob.config.secret_store().insert(psk_id.clone(), psk);
5010
5011        let commit = alice
5012            .commit_builder()
5013            .add_member(key_pkg)
5014            .unwrap()
5015            .add_external_psk(psk_id)
5016            .unwrap()
5017            .build()
5018            .await
5019            .unwrap();
5020
5021        bob.join_group(None, &commit.welcome_messages[0], None)
5022            .await
5023            .unwrap();
5024    }
5025
5026    #[cfg(feature = "by_ref_proposal")]
5027    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
5028    async fn invalid_update_does_not_prevent_other_updates() {
5029        const EXTENSION_TYPE: ExtensionType = ExtensionType::new(33);
5030
5031        let group_extensions = ExtensionList::from(vec![RequiredCapabilitiesExt {
5032            extensions: vec![EXTENSION_TYPE],
5033            ..Default::default()
5034        }
5035        .into_extension()
5036        .unwrap()]);
5037
5038        // Alice creates a group requiring support for an extension
5039        let mut alice = TestClientBuilder::new_for_test()
5040            .with_random_signing_identity("alice", TEST_CIPHER_SUITE)
5041            .await
5042            .extension_type(EXTENSION_TYPE)
5043            .build()
5044            .create_group(group_extensions.clone(), Default::default(), None)
5045            .await
5046            .unwrap();
5047
5048        let (bob_signing_identity, bob_secret_key) =
5049            get_test_signing_identity(TEST_CIPHER_SUITE, b"bob").await;
5050
5051        let bob_client = TestClientBuilder::new_for_test()
5052            .signing_identity(
5053                bob_signing_identity.clone(),
5054                bob_secret_key.clone(),
5055                TEST_CIPHER_SUITE,
5056            )
5057            .extension_type(EXTENSION_TYPE)
5058            .build();
5059
5060        let carol_client = TestClientBuilder::new_for_test()
5061            .with_random_signing_identity("carol", TEST_CIPHER_SUITE)
5062            .await
5063            .extension_type(EXTENSION_TYPE)
5064            .build();
5065
5066        let dave_client = TestClientBuilder::new_for_test()
5067            .with_random_signing_identity("dave", TEST_CIPHER_SUITE)
5068            .await
5069            .extension_type(EXTENSION_TYPE)
5070            .build();
5071
5072        // Alice adds Bob, Carol and Dave to the group. They all support the mandatory extension.
5073        let commit = alice
5074            .commit_builder()
5075            .add_member(
5076                bob_client
5077                    .generate_key_package_message(Default::default(), Default::default(), None)
5078                    .await
5079                    .unwrap(),
5080            )
5081            .unwrap()
5082            .add_member(
5083                carol_client
5084                    .generate_key_package_message(Default::default(), Default::default(), None)
5085                    .await
5086                    .unwrap(),
5087            )
5088            .unwrap()
5089            .add_member(
5090                dave_client
5091                    .generate_key_package_message(Default::default(), Default::default(), None)
5092                    .await
5093                    .unwrap(),
5094            )
5095            .unwrap()
5096            .build()
5097            .await
5098            .unwrap();
5099
5100        alice.apply_pending_commit().await.unwrap();
5101
5102        let mut bob = bob_client
5103            .join_group(None, &commit.welcome_messages[0], None)
5104            .await
5105            .unwrap()
5106            .0;
5107
5108        bob.write_to_storage().await.unwrap();
5109
5110        // Bob reloads his group data, but with parameters that will cause his generated leaves to
5111        // not support the mandatory extension.
5112        let mut bob = TestClientBuilder::new_for_test()
5113            .signing_identity(bob_signing_identity, bob_secret_key, TEST_CIPHER_SUITE)
5114            .key_package_repo(bob.config.key_package_repo())
5115            .group_state_storage(bob.config.group_state_storage())
5116            .build()
5117            .load_group(alice.group_id())
5118            .await
5119            .unwrap();
5120
5121        let mut carol = carol_client
5122            .join_group(None, &commit.welcome_messages[0], None)
5123            .await
5124            .unwrap()
5125            .0;
5126
5127        let mut dave = dave_client
5128            .join_group(None, &commit.welcome_messages[0], None)
5129            .await
5130            .unwrap()
5131            .0;
5132
5133        // Bob's updated leaf does not support the mandatory extension.
5134        let bob_update = bob.propose_update(Vec::new()).await.unwrap();
5135        let carol_update = carol.propose_update(Vec::new()).await.unwrap();
5136        let dave_update = dave.propose_update(Vec::new()).await.unwrap();
5137
5138        // Alice receives the update proposals to be committed.
5139        alice.process_incoming_message(bob_update).await.unwrap();
5140        alice.process_incoming_message(carol_update).await.unwrap();
5141        alice.process_incoming_message(dave_update).await.unwrap();
5142
5143        // Alice commits the update proposals.
5144        alice.commit(Vec::new()).await.unwrap();
5145
5146        let CommitEffect::NewEpoch(new_epoch) = alice.apply_pending_commit().await.unwrap().effect
5147        else {
5148            panic!("unexpected commit effect");
5149        };
5150
5151        let find_update_for = |id: &str| {
5152            new_epoch
5153                .applied_proposals
5154                .iter()
5155                .filter_map(|p| match p.proposal {
5156                    Proposal::Update(ref u) => u.signing_identity().credential.as_basic(),
5157                    _ => None,
5158                })
5159                .any(|c| c.identifier == id.as_bytes())
5160        };
5161
5162        // Carol's and Dave's updates should be part of the commit.
5163        assert!(find_update_for("carol"));
5164        assert!(find_update_for("dave"));
5165
5166        // Bob's update should be rejected.
5167        assert!(!find_update_for("bob"));
5168
5169        // Check that all members are still in the group.
5170        let all_members_are_in = alice
5171            .roster()
5172            .members_iter()
5173            .zip(["alice", "bob", "carol", "dave"])
5174            .all(|(member, id)| {
5175                member
5176                    .signing_identity
5177                    .credential
5178                    .as_basic()
5179                    .unwrap()
5180                    .identifier
5181                    == id.as_bytes()
5182            });
5183
5184        assert!(all_members_are_in);
5185    }
5186
5187    #[cfg(all(
5188        feature = "by_ref_proposal",
5189        feature = "custom_proposal",
5190        feature = "self_remove_proposal"
5191    ))]
5192    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
5193    async fn client_cannot_propose_self_remove_twice() {
5194        let mut alice = TestClientBuilder::new_for_test()
5195            .with_random_signing_identity("alice", TEST_CIPHER_SUITE)
5196            .await
5197            .custom_proposal_type(ProposalType::SELF_REMOVE)
5198            .build()
5199            .create_group(ExtensionList::new(), Default::default(), None)
5200            .await
5201            .unwrap();
5202
5203        alice.propose_self_remove(Vec::new()).await.unwrap();
5204        let again = alice.propose_self_remove(Vec::new()).await;
5205        assert_matches!(again, Err(MlsError::SelfRemoveAlreadyProposed));
5206    }
5207
5208    #[cfg(all(
5209        feature = "by_ref_proposal",
5210        feature = "custom_proposal",
5211        feature = "self_remove_proposal"
5212    ))]
5213    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
5214    async fn client_can_self_remove_and_another_client_can_commit() {
5215        let (mut alice, mut bob) = self_remove_group_setup().await;
5216
5217        let carol_client = TestClientBuilder::new_for_test()
5218            .with_random_signing_identity("carol", TEST_CIPHER_SUITE)
5219            .await
5220            .custom_proposal_type(ProposalType::SELF_REMOVE)
5221            .build();
5222
5223        // Alice adds Carol to the group.
5224        let commit = alice
5225            .commit_builder()
5226            .add_member(
5227                carol_client
5228                    .generate_key_package_message(Default::default(), Default::default(), None)
5229                    .await
5230                    .unwrap(),
5231            )
5232            .unwrap()
5233            .build()
5234            .await
5235            .unwrap();
5236
5237        alice.apply_pending_commit().await.unwrap();
5238        bob.process_incoming_message(commit.commit_message)
5239            .await
5240            .unwrap();
5241
5242        let mut carol = carol_client
5243            .join_group(None, &commit.welcome_messages[0], None)
5244            .await
5245            .unwrap()
5246            .0;
5247
5248        // Bob proposes self-remove.
5249        let bob_self_remove = bob.propose_self_remove(Vec::new()).await.unwrap();
5250
5251        // Alice receives the self-remove proposal to be committed.
5252        // Carol also receives the self-remove proposal. Carol will need this in order
5253        // to process the commit including the proposal, because self-remove proposals
5254        // are included by reference.
5255        alice
5256            .process_incoming_message(bob_self_remove.clone())
5257            .await
5258            .unwrap();
5259        carol
5260            .process_incoming_message(bob_self_remove)
5261            .await
5262            .unwrap();
5263
5264        // Alice commits Bob's self-remove.
5265        let commit = alice.commit(Vec::new()).await.unwrap();
5266        alice.apply_pending_commit().await.unwrap();
5267
5268        // Assert that after applying the commit removing Bob, that Bob is no longer in the group.
5269        let expected_members = vec![
5270            alice.member_at_index(alice.current_member_index()).unwrap(),
5271            carol.member_at_index(carol.current_member_index()).unwrap(),
5272        ];
5273        itertools::assert_equal(alice.roster().members_iter(), expected_members.clone());
5274
5275        // Assert that Carol can also process the commit and it removes Bob from the group.
5276        carol
5277            .process_incoming_message(commit.commit_message.clone())
5278            .await
5279            .unwrap();
5280        itertools::assert_equal(carol.roster().members_iter(), expected_members.clone());
5281        // Assert that Bob can process the commit.
5282        bob.process_incoming_message(commit.commit_message)
5283            .await
5284            .unwrap();
5285    }
5286
5287    #[cfg(all(
5288        feature = "by_ref_proposal",
5289        feature = "custom_proposal",
5290        feature = "self_remove_proposal"
5291    ))]
5292    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
5293    async fn commit_with_both_remove_and_self_remove_for_same_client_leaves_remove_unused() {
5294        let (mut alice, mut bob) = self_remove_group_setup().await;
5295
5296        // Bob proposes self-remove.
5297        let bob_self_remove = bob.propose_self_remove(Vec::new()).await.unwrap();
5298
5299        // Alice receives the self-remove proposal to be committed.
5300        alice
5301            .process_incoming_message(bob_self_remove.clone())
5302            .await
5303            .unwrap();
5304
5305        // Alice also removes Bob with a regular remove proposal.
5306        alice.propose_remove(1, Vec::new()).await.unwrap();
5307
5308        // Alice commits Bob's self-remove and Alice's removal of Bob. This filters out the remove proposal.
5309        let commit = alice.commit(Vec::new()).await.unwrap();
5310        let unused = &commit.unused_proposals[0];
5311
5312        let expected_index = LeafIndex::unchecked(1);
5313
5314        assert_matches!(
5315            unused,
5316            ProposalInfo {
5317                proposal: Proposal::Remove(RemoveProposal {
5318                    to_remove: i,
5319                }),
5320                sender: Sender::Member(0),
5321                ..
5322            }
5323        if *i == expected_index);
5324    }
5325
5326    #[cfg(all(
5327        feature = "by_ref_proposal",
5328        feature = "custom_proposal",
5329        feature = "self_remove_proposal"
5330    ))]
5331    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
5332    async fn client_processing_commit_with_self_remove_without_processing_proposal_first_errors() {
5333        let (mut alice, mut bob) = self_remove_group_setup().await;
5334
5335        let carol_client = TestClientBuilder::new_for_test()
5336            .with_random_signing_identity("carol", TEST_CIPHER_SUITE)
5337            .await
5338            .custom_proposal_type(ProposalType::SELF_REMOVE)
5339            .build();
5340
5341        // Alice adds Carol.
5342        let commit = alice
5343            .commit_builder()
5344            .add_member(
5345                carol_client
5346                    .generate_key_package_message(Default::default(), Default::default(), None)
5347                    .await
5348                    .unwrap(),
5349            )
5350            .unwrap()
5351            .build()
5352            .await
5353            .unwrap();
5354        let mut carol = carol_client
5355            .join_group(None, &commit.welcome_messages[0], None)
5356            .await
5357            .unwrap()
5358            .0;
5359        alice
5360            .process_incoming_message(commit.commit_message.clone())
5361            .await
5362            .unwrap();
5363        bob.process_incoming_message(commit.commit_message)
5364            .await
5365            .unwrap();
5366
5367        // Bob proposes self-remove.
5368        let bob_self_remove = bob.propose_self_remove(Vec::new()).await.unwrap();
5369
5370        // Alice receives the self-remove proposal to be committed.
5371        alice
5372            .process_incoming_message(bob_self_remove.clone())
5373            .await
5374            .unwrap();
5375
5376        let remove_bob_commit = alice.commit(Vec::new()).await.unwrap();
5377
5378        // Carol has not processed Bob's self-remove proposal,
5379        // and so the by-ref proposal is not found.
5380        let carol_attempts_commit_processing = carol
5381            .process_incoming_message(remove_bob_commit.commit_message)
5382            .await;
5383        assert_matches!(
5384            carol_attempts_commit_processing,
5385            Err(MlsError::ProposalNotFound)
5386        );
5387    }
5388
5389    #[cfg(all(
5390        feature = "by_ref_proposal",
5391        feature = "custom_proposal",
5392        feature = "self_remove_proposal"
5393    ))]
5394    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
5395    async fn external_commit_can_have_self_remove() {
5396        let (mut alice, mut bob) = self_remove_group_setup().await;
5397
5398        let carol_client = TestClientBuilder::new_for_test()
5399            .with_random_signing_identity("carol", TEST_CIPHER_SUITE)
5400            .await
5401            .custom_proposal_type(ProposalType::SELF_REMOVE)
5402            .build();
5403
5404        // Alice adds Carol.
5405        let commit = alice
5406            .commit_builder()
5407            .add_member(
5408                carol_client
5409                    .generate_key_package_message(Default::default(), Default::default(), None)
5410                    .await
5411                    .unwrap(),
5412            )
5413            .unwrap()
5414            .build()
5415            .await
5416            .unwrap();
5417        let mut carol = carol_client
5418            .join_group(None, &commit.welcome_messages[0], None)
5419            .await
5420            .unwrap()
5421            .0;
5422        alice
5423            .process_incoming_message(commit.commit_message.clone())
5424            .await
5425            .unwrap();
5426        bob.process_incoming_message(commit.commit_message)
5427            .await
5428            .unwrap();
5429
5430        let bob_self_remove = bob.propose_self_remove(Vec::new()).await.unwrap();
5431
5432        let group_info = alice
5433            .group_info_message_allowing_ext_commit(true)
5434            .await
5435            .unwrap();
5436        carol
5437            .process_incoming_message(bob_self_remove.clone())
5438            .await
5439            .unwrap();
5440        alice
5441            .process_incoming_message(bob_self_remove.clone())
5442            .await
5443            .unwrap();
5444
5445        let (mut carol_new_group, commit) = carol_client
5446            .external_commit_builder()
5447            .unwrap()
5448            .with_removal(carol.current_member_index())
5449            .with_received_custom_proposal(bob_self_remove)
5450            .build(group_info)
5451            .await
5452            .unwrap();
5453        bob.process_incoming_message(commit.clone()).await.unwrap();
5454        alice
5455            .process_incoming_message(commit.clone())
5456            .await
5457            .unwrap();
5458
5459        // Check that carol can decrypt a message in this new group
5460        let encrypted_message = alice
5461            .encrypt_application_message(b"test", vec![])
5462            .await
5463            .unwrap();
5464        carol_new_group
5465            .process_incoming_message(encrypted_message)
5466            .await
5467            .unwrap();
5468
5469        // Assert that after applying the commit removing Bob, that Bob is no longer in the group.
5470        let alice_identity = alice.current_member_signing_identity().unwrap();
5471        let carol_identity = carol_new_group.current_member_signing_identity().unwrap();
5472        let expected_member_identities = vec![alice_identity.clone(), carol_identity.clone()];
5473        itertools::assert_equal(
5474            alice.roster().members_iter().map(|m| m.signing_identity),
5475            expected_member_identities.clone(),
5476        );
5477        itertools::assert_equal(
5478            carol_new_group
5479                .roster()
5480                .members_iter()
5481                .map(|m| m.signing_identity),
5482            expected_member_identities.clone(),
5483        );
5484
5485        // Check that carol's signing identity has not changed.
5486        let carol_old_identity = carol_new_group.current_member_signing_identity().unwrap();
5487        assert!(carol_identity == carol_old_identity);
5488    }
5489
5490    #[cfg(all(
5491        feature = "by_ref_proposal",
5492        feature = "custom_proposal",
5493        feature = "self_remove_proposal"
5494    ))]
5495    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
5496    async fn can_build_external_commit_from_group_with_self_remove() {
5497        let (mut alice, mut bob) = self_remove_group_setup().await;
5498
5499        let carol_client = TestClientBuilder::new_for_test()
5500            .with_random_signing_identity("carol", TEST_CIPHER_SUITE)
5501            .await
5502            .custom_proposal_type(ProposalType::SELF_REMOVE)
5503            .build();
5504
5505        // Alice adds Carol.
5506        let commit = alice
5507            .commit_builder()
5508            .add_member(
5509                carol_client
5510                    .generate_key_package_message(Default::default(), Default::default(), None)
5511                    .await
5512                    .unwrap(),
5513            )
5514            .unwrap()
5515            .build()
5516            .await
5517            .unwrap();
5518        let carol = carol_client
5519            .join_group(None, &commit.welcome_messages[0], None)
5520            .await
5521            .unwrap()
5522            .0;
5523        alice
5524            .process_incoming_message(commit.commit_message.clone())
5525            .await
5526            .unwrap();
5527        bob.process_incoming_message(commit.commit_message)
5528            .await
5529            .unwrap();
5530
5531        let bob_self_remove = bob.propose_self_remove(Vec::new()).await.unwrap();
5532        alice
5533            .process_incoming_message(bob_self_remove.clone())
5534            .await
5535            .unwrap();
5536
5537        // Alice commits Bob's self-remove proposal
5538        let remove_bob_commit = alice.commit(Vec::new()).await.unwrap();
5539
5540        alice
5541            .process_incoming_message(remove_bob_commit.commit_message.clone())
5542            .await
5543            .unwrap();
5544
5545        let group_info = alice
5546            .group_info_message_allowing_ext_commit(true)
5547            .await
5548            .unwrap();
5549
5550        // Carol builds an external commit with Alice's group state, that includes Bob's self-remove committed.
5551        let (_, commit) = carol_client
5552            .external_commit_builder()
5553            .unwrap()
5554            .with_removal(carol.current_member_index())
5555            .build(group_info)
5556            .await
5557            .unwrap();
5558        alice
5559            .process_incoming_message(commit.clone())
5560            .await
5561            .unwrap();
5562    }
5563
5564    #[cfg(all(
5565        feature = "by_ref_proposal",
5566        feature = "custom_proposal",
5567        feature = "self_remove_proposal"
5568    ))]
5569    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
5570    async fn external_commit_can_have_multiple_self_removes() {
5571        let (mut alice, mut bob) = self_remove_group_setup().await;
5572
5573        let carol_client = TestClientBuilder::new_for_test()
5574            .with_random_signing_identity("carol", TEST_CIPHER_SUITE)
5575            .await
5576            .custom_proposal_type(ProposalType::SELF_REMOVE)
5577            .build();
5578
5579        // Alice adds Carol.
5580        let commit = alice
5581            .commit_builder()
5582            .add_member(
5583                carol_client
5584                    .generate_key_package_message(Default::default(), Default::default(), None)
5585                    .await
5586                    .unwrap(),
5587            )
5588            .unwrap()
5589            .build()
5590            .await
5591            .unwrap();
5592        let mut carol = carol_client
5593            .join_group(None, &commit.welcome_messages[0], None)
5594            .await
5595            .unwrap()
5596            .0;
5597        alice
5598            .process_incoming_message(commit.commit_message.clone())
5599            .await
5600            .unwrap();
5601        bob.process_incoming_message(commit.commit_message)
5602            .await
5603            .unwrap();
5604
5605        let bob_self_remove = bob.propose_self_remove(Vec::new()).await.unwrap();
5606        let alice_self_remove = alice.propose_self_remove(Vec::new()).await.unwrap();
5607
5608        let group_info = alice
5609            .group_info_message_allowing_ext_commit(true)
5610            .await
5611            .unwrap();
5612        carol
5613            .process_incoming_message(bob_self_remove.clone())
5614            .await
5615            .unwrap();
5616        alice
5617            .process_incoming_message(bob_self_remove.clone())
5618            .await
5619            .unwrap();
5620        bob.process_incoming_message(alice_self_remove.clone())
5621            .await
5622            .unwrap();
5623        carol
5624            .process_incoming_message(alice_self_remove.clone())
5625            .await
5626            .unwrap();
5627
5628        let (carol_new_group, commit) = carol_client
5629            .external_commit_builder()
5630            .unwrap()
5631            .with_removal(carol.current_member_index())
5632            .with_received_custom_proposal(bob_self_remove)
5633            .with_received_custom_proposal(alice_self_remove)
5634            .build(group_info)
5635            .await
5636            .unwrap();
5637
5638        carol
5639            .process_incoming_message(commit.clone())
5640            .await
5641            .unwrap();
5642        bob.process_incoming_message(commit.clone()).await.unwrap();
5643        alice.process_incoming_message(commit).await.unwrap();
5644
5645        // Assert that after applying the commit removing Bob, that Bob and Alice are no longer in the group.
5646        let carol_identity = carol_new_group.current_member_signing_identity().unwrap();
5647        let expected_member_identities = vec![carol_identity.clone()];
5648        itertools::assert_equal(
5649            carol_new_group
5650                .roster()
5651                .members_iter()
5652                .map(|m| m.signing_identity),
5653            expected_member_identities.clone(),
5654        );
5655    }
5656
5657    #[cfg(feature = "std")]
5658    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
5659    async fn commit_processes_with_custom_time() {
5660        // Corresponds to Wednesday, February 19, 2025 9:20:00 PM GMT
5661        let mut current_time: u64 = 1740000000;
5662
5663        let (bob_identity, secret_key) = get_test_signing_identity(TEST_CIPHER_SUITE, b"bob").await;
5664        let bob = TestClientBuilder::new_for_test()
5665            .signing_identity(bob_identity, secret_key, TEST_CIPHER_SUITE)
5666            .build();
5667
5668        current_time += 10;
5669
5670        let (alice_identity, secret_key) =
5671            get_test_signing_identity(TEST_CIPHER_SUITE, b"alice").await;
5672        let alice = TestClientBuilder::new_for_test()
5673            .signing_identity(alice_identity, secret_key, TEST_CIPHER_SUITE)
5674            .build();
5675
5676        current_time += 10;
5677
5678        let mut alice_group = alice
5679            .create_group(
5680                Default::default(),
5681                Default::default(),
5682                Some(current_time.into()),
5683            )
5684            .await
5685            .unwrap();
5686
5687        current_time += 10;
5688
5689        let bob_key_package = bob
5690            .generate_key_package_message(
5691                Default::default(),
5692                Default::default(),
5693                Some(current_time.into()),
5694            )
5695            .await
5696            .unwrap();
5697
5698        current_time += 10;
5699
5700        let commit_output = alice_group
5701            .commit_builder()
5702            .add_member(bob_key_package)
5703            .unwrap()
5704            .commit_time(current_time.into())
5705            .build()
5706            .await
5707            .unwrap();
5708
5709        current_time += 10;
5710
5711        alice_group
5712            .process_incoming_message_with_time(commit_output.commit_message, current_time.into())
5713            .await
5714            .unwrap();
5715    }
5716
5717    #[cfg(feature = "custom_proposal")]
5718    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
5719    async fn custom_proposal_may_enforce_path() {
5720        test_custom_proposal_mls_rules(true).await;
5721    }
5722
5723    #[cfg(feature = "custom_proposal")]
5724    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
5725    async fn custom_proposal_need_not_enforce_path() {
5726        test_custom_proposal_mls_rules(false).await;
5727    }
5728
5729    #[cfg(feature = "custom_proposal")]
5730    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
5731    async fn test_custom_proposal_mls_rules(path_required_for_custom: bool) {
5732        let mls_rules = CustomMlsRules {
5733            path_required_for_custom,
5734            external_joiner_can_send_custom: true,
5735        };
5736
5737        let mut alice = client_with_custom_rules(b"alice", mls_rules.clone())
5738            .await
5739            .create_group(Default::default(), Default::default(), None)
5740            .await
5741            .unwrap();
5742
5743        let alice_pub_before = alice.current_user_leaf_node().unwrap().public_key.clone();
5744
5745        let kp = client_with_custom_rules(b"bob", mls_rules)
5746            .await
5747            .generate_key_package_message(Default::default(), Default::default(), None)
5748            .await
5749            .unwrap();
5750
5751        alice
5752            .commit_builder()
5753            .custom_proposal(CustomProposal::new(TEST_CUSTOM_PROPOSAL_TYPE, vec![]))
5754            .add_member(kp)
5755            .unwrap()
5756            .build()
5757            .await
5758            .unwrap();
5759
5760        alice.apply_pending_commit().await.unwrap();
5761
5762        let alice_pub_after = &alice.current_user_leaf_node().unwrap().public_key;
5763
5764        if path_required_for_custom {
5765            assert_ne!(alice_pub_after, &alice_pub_before);
5766        } else {
5767            assert_eq!(alice_pub_after, &alice_pub_before);
5768        }
5769    }
5770
5771    #[cfg(feature = "custom_proposal")]
5772    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
5773    async fn custom_proposal_by_value_in_external_join_may_be_allowed() {
5774        test_custom_proposal_by_value_in_external_join(true).await
5775    }
5776
5777    #[cfg(feature = "custom_proposal")]
5778    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
5779    async fn custom_proposal_by_value_in_external_join_may_not_be_allowed() {
5780        test_custom_proposal_by_value_in_external_join(false).await
5781    }
5782
5783    #[cfg(feature = "custom_proposal")]
5784    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
5785    async fn test_custom_proposal_by_value_in_external_join(external_joiner_can_send_custom: bool) {
5786        let mls_rules = CustomMlsRules {
5787            path_required_for_custom: true,
5788            external_joiner_can_send_custom,
5789        };
5790
5791        let mut alice = client_with_custom_rules(b"alice", mls_rules.clone())
5792            .await
5793            .create_group(Default::default(), Default::default(), None)
5794            .await
5795            .unwrap();
5796
5797        let group_info = alice
5798            .group_info_message_allowing_ext_commit(true)
5799            .await
5800            .unwrap();
5801
5802        let commit = client_with_custom_rules(b"bob", mls_rules)
5803            .await
5804            .external_commit_builder()
5805            .unwrap()
5806            .with_custom_proposal(CustomProposal::new(TEST_CUSTOM_PROPOSAL_TYPE, vec![]))
5807            .build(group_info)
5808            .await;
5809
5810        if external_joiner_can_send_custom {
5811            let commit = commit.unwrap().1;
5812            alice.process_incoming_message(commit).await.unwrap();
5813        } else {
5814            assert_matches!(commit.map(|_| ()), Err(MlsError::MlsRulesError(_)));
5815        }
5816    }
5817
5818    #[cfg(feature = "custom_proposal")]
5819    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
5820    async fn custom_proposal_by_ref_in_external_join() {
5821        let mls_rules = CustomMlsRules {
5822            path_required_for_custom: true,
5823            external_joiner_can_send_custom: true,
5824        };
5825
5826        let mut alice = client_with_custom_rules(b"alice", mls_rules.clone())
5827            .await
5828            .create_group(Default::default(), Default::default(), None)
5829            .await
5830            .unwrap();
5831
5832        let by_ref = CustomProposal::new(TEST_CUSTOM_PROPOSAL_TYPE, vec![]);
5833        let by_ref = alice.propose_custom(by_ref, vec![]).await.unwrap();
5834
5835        let group_info = alice
5836            .group_info_message_allowing_ext_commit(true)
5837            .await
5838            .unwrap();
5839
5840        let (_, commit) = client_with_custom_rules(b"bob", mls_rules)
5841            .await
5842            .external_commit_builder()
5843            .unwrap()
5844            .with_received_custom_proposal(by_ref)
5845            .build(group_info)
5846            .await
5847            .unwrap();
5848
5849        alice.process_incoming_message(commit).await.unwrap();
5850    }
5851
5852    #[cfg(feature = "custom_proposal")]
5853    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
5854    async fn client_with_custom_rules(
5855        name: &[u8],
5856        mls_rules: CustomMlsRules,
5857    ) -> Client<impl MlsConfig> {
5858        let (signing_identity, signer) = get_test_signing_identity(TEST_CIPHER_SUITE, name).await;
5859
5860        ClientBuilder::new()
5861            .crypto_provider(TestCryptoProvider::new())
5862            .identity_provider(BasicWithCustomProvider::new(BasicIdentityProvider::new()))
5863            .signing_identity(signing_identity, signer, TEST_CIPHER_SUITE)
5864            .custom_proposal_type(TEST_CUSTOM_PROPOSAL_TYPE)
5865            .mls_rules(mls_rules)
5866            .build()
5867    }
5868
5869    #[derive(Debug, Clone)]
5870    struct CustomMlsRules {
5871        path_required_for_custom: bool,
5872        external_joiner_can_send_custom: bool,
5873    }
5874
5875    #[cfg(feature = "custom_proposal")]
5876    impl ProposalBundle {
5877        fn has_test_custom_proposal(&self) -> bool {
5878            self.custom_proposal_types()
5879                .any(|t| t == TEST_CUSTOM_PROPOSAL_TYPE)
5880        }
5881    }
5882
5883    #[cfg(feature = "custom_proposal")]
5884    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
5885    #[cfg_attr(mls_build_async, maybe_async::must_be_async)]
5886    impl crate::MlsRules for CustomMlsRules {
5887        type Error = MlsError;
5888
5889        fn commit_options(
5890            &self,
5891            _: &Roster,
5892            _: &GroupContext,
5893            proposals: &ProposalBundle,
5894        ) -> Result<CommitOptions, MlsError> {
5895            Ok(CommitOptions::default().with_path_required(
5896                !proposals.has_test_custom_proposal() || self.path_required_for_custom,
5897            ))
5898        }
5899
5900        fn encryption_options(
5901            &self,
5902            _: &Roster,
5903            _: &GroupContext,
5904        ) -> Result<crate::mls_rules::EncryptionOptions, MlsError> {
5905            Ok(Default::default())
5906        }
5907
5908        async fn filter_proposals(
5909            &self,
5910            _: CommitDirection,
5911            sender: CommitSource,
5912            _: &Roster,
5913            _: &GroupContext,
5914            proposals: ProposalBundle,
5915        ) -> Result<ProposalBundle, MlsError> {
5916            let is_external = matches!(sender, CommitSource::NewMember(_));
5917            let has_custom = proposals.has_test_custom_proposal();
5918            let allowed = !has_custom || !is_external || self.external_joiner_can_send_custom;
5919
5920            allowed.then_some(proposals).ok_or(MlsError::InvalidSender)
5921        }
5922    }
5923
5924    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
5925    async fn group_can_receive_commit_from_self() {
5926        let mut group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
5927
5928        let commit = group.commit(vec![]).await.unwrap();
5929
5930        let update = group
5931            .process_incoming_message(commit.commit_message)
5932            .await
5933            .unwrap();
5934
5935        let ReceivedMessage::Commit(update) = update else {
5936            panic!("expected commit message")
5937        };
5938
5939        assert_eq!(update.committer, *group.private_tree.self_index);
5940    }
5941
5942    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
5943    async fn can_process_commit_when_pending_commit() {
5944        let mut groups = test_n_member_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, 2).await;
5945
5946        let commit = groups[0].commit(vec![]).await.unwrap().commit_message;
5947        groups[1].commit(vec![]).await.unwrap();
5948
5949        groups[1].process_incoming_message(commit).await.unwrap();
5950
5951        let res = groups[1].apply_pending_commit().await;
5952        assert_matches!(res, Err(MlsError::PendingCommitNotFound));
5953    }
5954
5955    #[cfg(feature = "by_ref_proposal")]
5956    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
5957    async fn can_process_own_plaintext_proposal() {
5958        can_process_own_proposal(false).await;
5959    }
5960
5961    #[cfg(feature = "by_ref_proposal")]
5962    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
5963    async fn can_process_own_ciphertext_proposal() {
5964        can_process_own_proposal(true).await;
5965    }
5966
5967    #[cfg(feature = "by_ref_proposal")]
5968    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
5969    async fn can_process_own_proposal(encrypt_proposal: bool) {
5970        let (alice, _) = test_client_with_key_pkg_custom(
5971            TEST_PROTOCOL_VERSION,
5972            TEST_CIPHER_SUITE,
5973            "alice",
5974            Default::default(),
5975            Default::default(),
5976            |c| c.0.mls_rules.encryption_options.encrypt_control_messages = encrypt_proposal,
5977        )
5978        .await;
5979
5980        let mut alice = TestGroup {
5981            group: alice
5982                .create_group(Default::default(), Default::default(), None)
5983                .await
5984                .unwrap(),
5985        };
5986
5987        let mut bob = alice.join("bob").await.0;
5988        let mut alice = alice;
5989
5990        let upd = alice.propose_update(vec![]).await.unwrap();
5991        alice.process_incoming_message(upd.clone()).await.unwrap();
5992
5993        bob.process_incoming_message(upd).await.unwrap();
5994        let commit = bob.commit(vec![]).await.unwrap().commit_message;
5995        let update = alice.process_incoming_message(commit).await.unwrap();
5996
5997        let ReceivedMessage::Commit(CommitMessageDescription {
5998            effect: CommitEffect::NewEpoch(new_epoch),
5999            ..
6000        }) = update
6001        else {
6002            panic!("unexpected commit effect")
6003        };
6004
6005        assert_eq!(new_epoch.applied_proposals.len(), 1);
6006        assert_eq!(new_epoch.applied_proposals[0].sender, Sender::Member(0));
6007    }
6008
6009    #[cfg(feature = "by_ref_proposal")]
6010    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
6011    async fn commit_clears_proposals() {
6012        let mut groups = test_n_member_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, 2).await;
6013
6014        groups[0].propose_update(vec![]).await.unwrap();
6015
6016        assert_eq!(groups[0].state.proposals.proposals.len(), 1);
6017        assert_eq!(groups[0].state.proposals.own_proposals.len(), 1);
6018
6019        let commit = groups[1].commit(vec![]).await.unwrap().commit_message;
6020        groups[0].process_message(commit).await.unwrap();
6021
6022        assert!(groups[0].state.proposals.proposals.is_empty());
6023        assert!(groups[0].state.proposals.own_proposals.is_empty());
6024    }
6025
6026    #[cfg(feature = "by_ref_proposal")]
6027    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
6028    async fn commit_required_is_true_when_proposals_pending() {
6029        let mut group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
6030
6031        assert!(!group.commit_required());
6032
6033        group
6034            .propose_group_context_extensions(ExtensionList::new(), vec![])
6035            .await
6036            .unwrap();
6037
6038        assert!(group.commit_required());
6039
6040        let res = group.encrypt_application_message(&[0u8; 32], vec![]).await;
6041
6042        assert_matches!(res, Err(MlsError::CommitRequired));
6043
6044        group.commit(vec![]).await.unwrap();
6045        group.apply_pending_commit().await.unwrap();
6046
6047        assert!(!group.commit_required());
6048    }
6049
6050    // Testing with std is sufficient. Non-std creates incompatible storage and a lot of special cases.
6051    #[cfg(feature = "std")]
6052    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
6053    async fn can_be_stored_without_tree() {
6054        let mut group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
6055        let storage = group.config.group_state_storage().inner;
6056
6057        group.write_to_storage().await.unwrap();
6058        let snapshot_with_tree = storage.lock().unwrap().drain().next().unwrap().1;
6059
6060        group.write_to_storage_without_ratchet_tree().await.unwrap();
6061        let snapshot_without_tree = storage.lock().unwrap().iter().next().unwrap().1.clone();
6062
6063        let tree = group.state.public_tree.nodes.mls_encode_to_vec().unwrap();
6064        let empty_tree = Vec::<u8>::new().mls_encode_to_vec().unwrap();
6065
6066        assert_eq!(
6067            snapshot_with_tree.state_data.len() - snapshot_without_tree.state_data.len(),
6068            tree.len() - empty_tree.len()
6069        );
6070
6071        let exported_tree = group.export_tree();
6072
6073        let restored = Client::new(group.config.clone(), None, None, TEST_PROTOCOL_VERSION)
6074            .load_group_with_ratchet_tree(group.group_id(), exported_tree)
6075            .await
6076            .unwrap();
6077
6078        assert_eq!(restored.group_state(), group.group_state());
6079    }
6080
6081    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
6082    async fn delete_exporter() {
6083        let mut group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
6084
6085        group.export_secret(b"123", b"", 15).await.unwrap();
6086
6087        group.delete_exporter();
6088        let res = group.export_secret(b"123", b"", 15).await;
6089        assert_matches!(res, Err(MlsError::ExporterDeleted));
6090
6091        group.commit(vec![]).await.unwrap();
6092        group.apply_pending_commit().await.unwrap();
6093        group.export_secret(b"123", b"", 15).await.unwrap();
6094    }
6095}