Skip to main content

mls_rs/
client.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 crate::cipher_suite::CipherSuite;
6use crate::client_builder::{recreate_config, BaseConfig, ClientBuilder, MakeConfig};
7use crate::client_config::ClientConfig;
8use crate::group::framing::MlsMessage;
9
10use crate::group::{cipher_suite_provider, validate_group_info_joiner, GroupInfo};
11use crate::group::{
12    framing::MlsMessagePayload, snapshot::Snapshot, ExportedTree, Group, NewMemberInfo,
13};
14#[cfg(feature = "by_ref_proposal")]
15use crate::group::{
16    framing::{Content, PublicMessage, Sender, WireFormat},
17    message_signature::AuthenticatedContent,
18    proposal::{AddProposal, Proposal},
19};
20use crate::identity::SigningIdentity;
21use crate::key_package::{KeyPackageGeneration, KeyPackageGenerator};
22use crate::protocol_version::ProtocolVersion;
23use crate::time::MlsTime;
24use crate::tree_kem::node::NodeIndex;
25use alloc::vec::Vec;
26use mls_rs_codec::MlsDecode;
27use mls_rs_core::crypto::{CryptoProvider, SignatureSecretKey};
28use mls_rs_core::error::{AnyError, IntoAnyError};
29use mls_rs_core::extension::{ExtensionError, ExtensionList, ExtensionType};
30use mls_rs_core::group::{GroupStateStorage, ProposalType};
31use mls_rs_core::identity::{CredentialType, IdentityProvider, MemberValidationContext};
32use mls_rs_core::key_package::KeyPackageStorage;
33
34use crate::group::external_commit::ExternalCommitBuilder;
35
36#[cfg(feature = "by_ref_proposal")]
37use alloc::boxed::Box;
38
39#[derive(Debug)]
40#[cfg_attr(feature = "std", derive(thiserror::Error))]
41#[non_exhaustive]
42pub enum MlsError {
43    #[cfg_attr(feature = "std", error(transparent))]
44    IdentityProviderError(AnyError),
45    #[cfg_attr(feature = "std", error(transparent))]
46    CryptoProviderError(AnyError),
47    #[cfg_attr(feature = "std", error(transparent))]
48    KeyPackageRepoError(AnyError),
49    #[cfg_attr(feature = "std", error(transparent))]
50    GroupStorageError(AnyError),
51    #[cfg_attr(feature = "std", error(transparent))]
52    PskStoreError(AnyError),
53    #[cfg_attr(feature = "std", error(transparent))]
54    MlsRulesError(AnyError),
55    #[cfg_attr(feature = "std", error(transparent))]
56    SerializationError(AnyError),
57    #[cfg_attr(feature = "std", error(transparent))]
58    ExtensionError(AnyError),
59    #[cfg_attr(feature = "std", error("Cipher suite does not match"))]
60    CipherSuiteMismatch,
61    #[cfg_attr(feature = "std", error("Initial epoch must be 1"))]
62    InitialEpochNotOne,
63    #[cfg_attr(feature = "std", error("Invalid commit, missing required path"))]
64    CommitMissingPath,
65    #[cfg_attr(feature = "std", error("plaintext message for incorrect epoch"))]
66    InvalidEpoch,
67    #[cfg_attr(feature = "std", error("invalid signature found"))]
68    InvalidSignature,
69    #[cfg_attr(feature = "std", error("invalid confirmation tag"))]
70    InvalidConfirmationTag,
71    #[cfg_attr(feature = "std", error("invalid membership tag"))]
72    InvalidMembershipTag,
73    #[cfg_attr(feature = "std", error("corrupt private key, missing required values"))]
74    InvalidTreeKemPrivateKey,
75    #[cfg_attr(feature = "std", error("key package not found, unable to process"))]
76    WelcomeKeyPackageNotFound,
77    #[cfg_attr(feature = "std", error("leaf not found in tree for index {0}"))]
78    LeafNotFound(u32),
79    #[cfg_attr(feature = "std", error("message from self can't be processed"))]
80    CantProcessMessageFromSelf,
81    #[cfg_attr(
82        feature = "std",
83        error("pending proposals found, commit required before application messages can be sent")
84    )]
85    CommitRequired,
86    #[cfg_attr(
87        feature = "std",
88        error("ratchet tree not provided or discovered in GroupInfo")
89    )]
90    RatchetTreeNotFound,
91    #[cfg_attr(feature = "std", error("External sender cannot commit"))]
92    ExternalSenderCannotCommit,
93    #[cfg_attr(feature = "std", error("Unsupported protocol version {0:?}"))]
94    UnsupportedProtocolVersion(ProtocolVersion),
95    #[cfg_attr(feature = "std", error("Protocol version mismatch"))]
96    ProtocolVersionMismatch,
97    #[cfg_attr(feature = "std", error("Unsupported cipher suite {0:?}"))]
98    UnsupportedCipherSuite(CipherSuite),
99    #[cfg_attr(feature = "std", error("Signing key of external sender is unknown"))]
100    UnknownSigningIdentityForExternalSender,
101    #[cfg_attr(
102        feature = "std",
103        error("External proposals are disabled for this group")
104    )]
105    ExternalProposalsDisabled,
106    #[cfg_attr(
107        feature = "std",
108        error("Signing identity is not allowed to externally propose")
109    )]
110    InvalidExternalSigningIdentity,
111    #[cfg_attr(feature = "std", error("Missing ExternalPub extension"))]
112    MissingExternalPubExtension,
113    #[cfg_attr(feature = "std", error("Epoch not found"))]
114    EpochNotFound,
115    #[cfg_attr(feature = "std", error("Unencrypted application message"))]
116    UnencryptedApplicationMessage,
117    #[cfg_attr(
118        feature = "std",
119        error("NewMemberCommit sender type can only be used to send Commit content")
120    )]
121    ExpectedCommitForNewMemberCommit,
122    #[cfg_attr(
123        feature = "std",
124        error("NewMemberProposal sender type can only be used to send add proposals")
125    )]
126    ExpectedAddProposalForNewMemberProposal,
127    #[cfg_attr(
128        feature = "std",
129        error("External commit missing ExternalInit proposal")
130    )]
131    ExternalCommitMissingExternalInit,
132    #[cfg_attr(
133        feature = "std",
134        error(
135            "A ReIinit has been applied. The next action must be creating or receiving a welcome."
136        )
137    )]
138    GroupUsedAfterReInit,
139    #[cfg_attr(feature = "std", error("Pending ReIinit not found."))]
140    PendingReInitNotFound,
141    #[cfg_attr(
142        feature = "std",
143        error("The extensions in the welcome message and in the reinit do not match.")
144    )]
145    ReInitExtensionsMismatch,
146    #[cfg_attr(feature = "std", error("signer not found for given identity"))]
147    SignerNotFound,
148    #[cfg_attr(feature = "std", error("commit already pending"))]
149    ExistingPendingCommit,
150    #[cfg_attr(feature = "std", error("pending commit not found"))]
151    PendingCommitNotFound,
152    #[cfg_attr(feature = "std", error("unexpected message type for action"))]
153    UnexpectedMessageType,
154    #[cfg_attr(
155        feature = "std",
156        error("membership tag on MlsPlaintext for non-member sender")
157    )]
158    MembershipTagForNonMember,
159    #[cfg_attr(feature = "std", error("No member found for given identity id."))]
160    MemberNotFound,
161    #[cfg_attr(feature = "std", error("group not found"))]
162    GroupNotFound,
163    #[cfg_attr(feature = "std", error("unexpected PSK ID"))]
164    UnexpectedPskId,
165    #[cfg_attr(feature = "std", error("invalid sender for content type"))]
166    InvalidSender,
167    #[cfg_attr(feature = "std", error("GroupID mismatch"))]
168    GroupIdMismatch,
169    #[cfg_attr(feature = "std", error("storage retention can not be zero"))]
170    NonZeroRetentionRequired,
171    #[cfg_attr(feature = "std", error("Too many PSK IDs to compute PSK secret"))]
172    TooManyPskIds,
173    #[cfg_attr(feature = "std", error("Missing required Psk"))]
174    MissingRequiredPsk,
175    #[cfg_attr(feature = "std", error("Old group state not found"))]
176    OldGroupStateNotFound,
177    #[cfg_attr(feature = "std", error("leaf secret already consumed"))]
178    InvalidLeafConsumption,
179    #[cfg_attr(feature = "std", error("key not available, invalid generation {0}"))]
180    KeyMissing(u32),
181    #[cfg_attr(
182        feature = "std",
183        error("requested generation {0} is too far ahead of current generation")
184    )]
185    InvalidFutureGeneration(u32),
186    #[cfg_attr(feature = "std", error("leaf node has no children"))]
187    LeafNodeNoChildren,
188    #[cfg_attr(feature = "std", error("root node has no parent"))]
189    LeafNodeNoParent,
190    #[cfg_attr(feature = "std", error("index out of range"))]
191    InvalidTreeIndex,
192    #[cfg_attr(feature = "std", error("time overflow"))]
193    TimeOverflow,
194    #[cfg_attr(feature = "std", error("invalid leaf_node_source"))]
195    InvalidLeafNodeSource,
196    #[cfg_attr(
197        feature = "std",
198        error("current time ({}) is not within key package lifetime ({} to {})",
199              timestamp.seconds_since_epoch(),
200              not_before.seconds_since_epoch(),
201              not_after.seconds_since_epoch(),
202        )
203    )]
204    InvalidLifetime {
205        not_before: MlsTime,
206        not_after: MlsTime,
207        timestamp: MlsTime,
208    },
209    #[cfg_attr(feature = "std", error("required extension not found"))]
210    RequiredExtensionNotFound(ExtensionType),
211    #[cfg_attr(feature = "std", error("required proposal not found"))]
212    RequiredProposalNotFound(ProposalType),
213    #[cfg_attr(feature = "std", error("required credential not found"))]
214    RequiredCredentialNotFound(CredentialType),
215    #[cfg_attr(feature = "std", error("capabilities must describe extensions used"))]
216    ExtensionNotInCapabilities(ExtensionType),
217    #[cfg_attr(feature = "std", error("expected non-blank node"))]
218    ExpectedNode,
219    #[cfg_attr(feature = "std", error("node index is out of bounds {0}"))]
220    InvalidNodeIndex(NodeIndex),
221    #[cfg_attr(feature = "std", error("unexpected empty node found"))]
222    UnexpectedEmptyNode,
223    #[cfg_attr(
224        feature = "std",
225        error("duplicate signature key, hpke key or identity found at index {0}")
226    )]
227    DuplicateLeafData(u32),
228    #[cfg_attr(
229        feature = "std",
230        error("In-use credential type not supported by new leaf at index")
231    )]
232    InUseCredentialTypeUnsupportedByNewLeaf,
233    #[cfg_attr(
234        feature = "std",
235        error("Not all members support the credential type used by new leaf")
236    )]
237    CredentialTypeOfNewLeafIsUnsupported,
238    #[cfg_attr(
239        feature = "std",
240        error("the length of the update path is different than the length of the direct path")
241    )]
242    WrongPathLen,
243    #[cfg_attr(
244        feature = "std",
245        error("same HPKE leaf key before and after applying the update path for leaf {0}")
246    )]
247    SameHpkeKey(u32),
248    #[cfg_attr(feature = "std", error("init key is not valid for cipher suite"))]
249    InvalidInitKey,
250    #[cfg_attr(
251        feature = "std",
252        error("init key can not be equal to leaf node public key")
253    )]
254    InitLeafKeyEquality,
255    #[cfg_attr(feature = "std", error("different identity in update for leaf {0}"))]
256    DifferentIdentityInUpdate(u32),
257    #[cfg_attr(feature = "std", error("update path pub key mismatch"))]
258    PubKeyMismatch,
259    #[cfg_attr(feature = "std", error("tree hash mismatch"))]
260    TreeHashMismatch,
261    #[cfg_attr(feature = "std", error("bad update: no suitable secret key"))]
262    UpdateErrorNoSecretKey,
263    #[cfg_attr(feature = "std", error("invalid lca, not found on direct path"))]
264    LcaNotFoundInDirectPath,
265    #[cfg_attr(feature = "std", error("update path parent hash mismatch"))]
266    ParentHashMismatch,
267    #[cfg_attr(feature = "std", error("unexpected pattern of unmerged leaves"))]
268    UnmergedLeavesMismatch,
269    #[cfg_attr(feature = "std", error("empty tree"))]
270    UnexpectedEmptyTree,
271    #[cfg_attr(feature = "std", error("trailing blanks"))]
272    UnexpectedTrailingBlanks,
273    // Proposal Rules errors
274    #[cfg_attr(
275        feature = "std",
276        error("Commiter must not include any update proposals generated by the commiter")
277    )]
278    InvalidCommitSelfUpdate,
279    #[cfg_attr(feature = "std", error("A PreSharedKey proposal must have a PSK of type External or type Resumption and usage Application"))]
280    InvalidTypeOrUsageInPreSharedKeyProposal,
281    #[cfg_attr(feature = "std", error("psk nonce length does not match cipher suite"))]
282    InvalidPskNonceLength,
283    #[cfg_attr(
284        feature = "std",
285        error("ReInit proposal protocol version is less than the version of the original group")
286    )]
287    InvalidProtocolVersionInReInit,
288    #[cfg_attr(feature = "std", error("More than one proposal applying to leaf: {0}"))]
289    MoreThanOneProposalForLeaf(u32),
290    #[cfg_attr(
291        feature = "std",
292        error("More than one GroupContextExtensions proposal")
293    )]
294    MoreThanOneGroupContextExtensionsProposal,
295    #[cfg_attr(feature = "std", error("Invalid proposal type for sender"))]
296    InvalidProposalTypeForSender,
297    #[cfg_attr(
298        feature = "std",
299        error("External commit must have exactly one ExternalInit proposal")
300    )]
301    ExternalCommitMustHaveExactlyOneExternalInit,
302    #[cfg_attr(feature = "std", error("External commit must have a new leaf"))]
303    ExternalCommitMustHaveNewLeaf,
304    #[cfg_attr(
305        feature = "std",
306        error("External commit contains removal of other identity")
307    )]
308    ExternalCommitRemovesOtherIdentity,
309    #[cfg_attr(
310        feature = "std",
311        error("External commit contains more than one Remove proposal")
312    )]
313    ExternalCommitWithMoreThanOneRemove,
314    #[cfg_attr(feature = "std", error("Duplicate PSK IDs"))]
315    DuplicatePskIds,
316    #[cfg_attr(
317        feature = "std",
318        error("Invalid proposal type {0:?} in external commit")
319    )]
320    InvalidProposalTypeInExternalCommit(ProposalType),
321    #[cfg_attr(feature = "std", error("Committer can not remove themselves"))]
322    CommitterSelfRemoval,
323    #[cfg_attr(
324        feature = "std",
325        error("Only members can commit proposals by reference")
326    )]
327    OnlyMembersCanCommitProposalsByRef,
328    #[cfg_attr(feature = "std", error("Other proposal with ReInit"))]
329    OtherProposalWithReInit,
330    #[cfg_attr(feature = "std", error("Unsupported group extension {0:?}"))]
331    UnsupportedGroupExtension(ExtensionType),
332    #[cfg_attr(feature = "std", error("Unsupported custom proposal type {0:?}"))]
333    UnsupportedCustomProposal(ProposalType),
334    #[cfg_attr(feature = "std", error("by-ref proposal not found"))]
335    ProposalNotFound,
336    #[cfg_attr(
337        feature = "std",
338        error("Removing non-existing member (or removing a member twice)")
339    )]
340    RemovingNonExistingMember,
341    #[cfg_attr(feature = "std", error("Updated identity not a valid successor"))]
342    InvalidSuccessor,
343    #[cfg_attr(
344        feature = "std",
345        error("Updating non-existing member (or updating a member twice)")
346    )]
347    UpdatingNonExistingMember,
348    #[cfg_attr(feature = "std", error("Failed generating next path secret"))]
349    FailedGeneratingPathSecret,
350    #[cfg_attr(feature = "std", error("Invalid group info"))]
351    InvalidGroupInfo,
352    #[cfg_attr(feature = "std", error("Invalid welcome message"))]
353    InvalidWelcomeMessage,
354    #[cfg_attr(feature = "std", error("Exporter deleted"))]
355    ExporterDeleted,
356    #[cfg_attr(feature = "std", error("Self-remove already proposed"))]
357    SelfRemoveAlreadyProposed,
358    #[cfg_attr(feature = "std", error("Default value listed"))]
359    DefaultValueListed,
360    #[cfg_attr(feature = "std", error("not a subgroup"))]
361    NotASubgroup,
362}
363
364impl IntoAnyError for MlsError {
365    #[cfg(feature = "std")]
366    fn into_dyn_error(self) -> Result<Box<dyn std::error::Error + Send + Sync>, Self> {
367        Ok(self.into())
368    }
369}
370
371impl From<mls_rs_codec::Error> for MlsError {
372    #[inline]
373    fn from(e: mls_rs_codec::Error) -> Self {
374        MlsError::SerializationError(e.into_any_error())
375    }
376}
377
378impl From<ExtensionError> for MlsError {
379    #[inline]
380    fn from(e: ExtensionError) -> Self {
381        MlsError::ExtensionError(e.into_any_error())
382    }
383}
384
385/// MLS client used to create key packages and manage groups.
386///
387/// [`Client::builder`] can be used to instantiate it.
388///
389/// Clients are able to support multiple protocol versions, ciphersuites
390/// and underlying identities used to join groups and generate key packages.
391/// Applications may decide to create one or many clients depending on their
392/// specific needs.
393#[derive(Clone, Debug)]
394pub struct Client<C> {
395    pub(crate) config: C,
396    pub(crate) signing_identity: Option<(SigningIdentity, CipherSuite)>,
397    pub(crate) signer: Option<SignatureSecretKey>,
398    pub(crate) version: ProtocolVersion,
399}
400
401impl Client<()> {
402    /// Returns a [`ClientBuilder`]
403    /// used to configure client preferences and providers.
404    pub fn builder() -> ClientBuilder<BaseConfig> {
405        ClientBuilder::new()
406    }
407}
408
409impl<C> Client<C>
410where
411    C: ClientConfig + Clone,
412{
413    pub(crate) fn new(
414        config: C,
415        signer: Option<SignatureSecretKey>,
416        signing_identity: Option<(SigningIdentity, CipherSuite)>,
417        version: ProtocolVersion,
418    ) -> Self {
419        Client {
420            config,
421            signer,
422            signing_identity,
423            version,
424        }
425    }
426
427    pub fn to_builder(&self, timestamp: Option<MlsTime>) -> ClientBuilder<MakeConfig<C>> {
428        ClientBuilder::from_config(recreate_config(
429            self.config.clone(),
430            self.signer.clone(),
431            self.signing_identity.clone(),
432            self.version,
433            timestamp,
434        ))
435    }
436
437    /// Creates a new key package message that can be used to to add this
438    /// client to a [Group](crate::group::Group). Each call to this function
439    /// will produce a unique value that is signed by `signing_identity`.
440    ///
441    /// The secret keys for the resulting key package message will be stored in
442    /// the [KeyPackageStorage](crate::KeyPackageStorage)
443    /// that was used to configure the client and will
444    /// automatically be erased when this key package is used to
445    /// [join a group](Client::join_group).
446    ///
447    /// # Warning
448    ///
449    /// A key package message may only be used once.
450    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
451    pub async fn generate_key_package_message(
452        &self,
453        key_package_extensions: ExtensionList,
454        leaf_node_extensions: ExtensionList,
455        timestamp: Option<MlsTime>,
456    ) -> Result<MlsMessage, MlsError> {
457        Ok(self
458            .generate_key_package(key_package_extensions, leaf_node_extensions, timestamp)
459            .await?
460            .key_package_message())
461    }
462
463    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
464    async fn generate_key_package(
465        &self,
466        key_package_extensions: ExtensionList,
467        leaf_node_extensions: ExtensionList,
468        timestamp: Option<MlsTime>,
469    ) -> Result<KeyPackageGeneration, MlsError> {
470        let (signing_identity, cipher_suite) = self.signing_identity()?;
471
472        let cipher_suite_provider = self
473            .config
474            .crypto_provider()
475            .cipher_suite_provider(cipher_suite)
476            .ok_or(MlsError::UnsupportedCipherSuite(cipher_suite))?;
477
478        let key_package_generator = KeyPackageGenerator {
479            protocol_version: self.version,
480            cipher_suite_provider: &cipher_suite_provider,
481            signing_key: self.signer()?,
482            signing_identity,
483        };
484
485        let key_pkg_gen = key_package_generator
486            .generate(
487                self.config.lifetime(timestamp),
488                self.config.capabilities(),
489                key_package_extensions,
490                leaf_node_extensions,
491            )
492            .await?;
493
494        key_pkg_gen
495            .key_package
496            .leaf_node
497            .validate_no_default_values_listed()?;
498
499        let (id, key_package_data) = key_pkg_gen.to_storage()?;
500
501        self.config
502            .key_package_repo()
503            .insert(id, key_package_data)
504            .await
505            .map_err(|e| MlsError::KeyPackageRepoError(e.into_any_error()))?;
506
507        Ok(key_pkg_gen)
508    }
509
510    /// Create a group with a specific group_id.
511    ///
512    /// This function behaves the same way as
513    /// [create_group](Client::create_group) except that it
514    /// specifies a specific unique group identifier to be used.
515    ///
516    /// # Warning
517    ///
518    /// It is recommended to use [create_group](Client::create_group)
519    /// instead of this function because it guarantees that group_id values
520    /// are globally unique.
521    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
522    pub async fn create_group_with_id(
523        &self,
524        group_id: Vec<u8>,
525        group_context_extensions: ExtensionList,
526        leaf_node_extensions: ExtensionList,
527        timestamp: Option<MlsTime>,
528    ) -> Result<Group<C>, MlsError> {
529        let (signing_identity, cipher_suite) = self.signing_identity()?;
530
531        Group::new(
532            self.config.clone(),
533            Some(group_id),
534            cipher_suite,
535            self.version,
536            signing_identity.clone(),
537            group_context_extensions,
538            leaf_node_extensions,
539            self.signer()?.clone(),
540            timestamp,
541        )
542        .await
543    }
544
545    /// Create a MLS group.
546    ///
547    /// The `cipher_suite` provided must be supported by the
548    /// [CipherSuiteProvider](crate::CipherSuiteProvider)
549    /// that was used to build the client.
550    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
551    pub async fn create_group(
552        &self,
553        group_context_extensions: ExtensionList,
554        leaf_node_extensions: ExtensionList,
555        timestamp: Option<MlsTime>,
556    ) -> Result<Group<C>, MlsError> {
557        let (signing_identity, cipher_suite) = self.signing_identity()?;
558
559        Group::new(
560            self.config.clone(),
561            None,
562            cipher_suite,
563            self.version,
564            signing_identity.clone(),
565            group_context_extensions,
566            leaf_node_extensions,
567            self.signer()?.clone(),
568            timestamp,
569        )
570        .await
571    }
572
573    /// Join a MLS group via a welcome message created by a
574    /// [Commit](crate::group::CommitOutput).
575    ///
576    /// `tree_data` is required to be provided out of band if the client that
577    /// created `welcome_message` did not use the `ratchet_tree_extension`
578    /// according to [`MlsRules::commit_options`](`crate::MlsRules::commit_options`).
579    /// at the time the welcome message was created. `tree_data` can
580    /// be exported from a group using the
581    /// [export tree function](crate::group::Group::export_tree).
582    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
583    pub async fn join_group(
584        &self,
585        tree_data: Option<ExportedTree<'_>>,
586        welcome_message: &MlsMessage,
587        maybe_time: Option<MlsTime>,
588    ) -> Result<(Group<C>, NewMemberInfo), MlsError> {
589        Group::join(
590            welcome_message,
591            tree_data,
592            self.config.clone(),
593            self.signer()?.clone(),
594            maybe_time,
595        )
596        .await
597    }
598
599    /// Decrypt GroupInfo encrypted in the Welcome message without actually joining
600    /// the group. The ratchet tree is not needed.
601    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
602    pub async fn examine_welcome_message(
603        &self,
604        welcome_message: &MlsMessage,
605    ) -> Result<GroupInfo, MlsError> {
606        Group::decrypt_group_info(welcome_message, &self.config).await
607    }
608
609    /// Validate GroupInfo message. This does NOT validate the ratchet tree in case
610    /// it is provided in the extension. It validates the signature, identity of the
611    /// signer, identities of external senders and cipher suite.
612    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
613    pub async fn validate_group_info(
614        &self,
615        group_info_message: &MlsMessage,
616        signer: &SigningIdentity,
617    ) -> Result<(), MlsError> {
618        let MlsMessagePayload::GroupInfo(group_info) = &group_info_message.payload else {
619            return Err(MlsError::UnexpectedMessageType);
620        };
621
622        let cs = cipher_suite_provider(
623            self.config.crypto_provider(),
624            group_info.group_context.cipher_suite,
625        )?;
626
627        let id = self.config.identity_provider();
628
629        validate_group_info_joiner(group_info_message.version, group_info, signer, &id, &cs)
630            .await?;
631
632        let context = MemberValidationContext::ForNewGroup {
633            current_context: &group_info.group_context,
634        };
635
636        id.validate_member(signer, None, context)
637            .await
638            .map_err(|e| MlsError::IdentityProviderError(e.into_any_error()))?;
639
640        Ok(())
641    }
642
643    /// 0-RTT add to an existing [group](crate::group::Group)
644    ///
645    /// External commits allow for immediate entry into a
646    /// [group](crate::group::Group), even if all of the group members
647    /// are currently offline and unable to process messages. Sending an
648    /// external commit is only allowed for groups that have provided
649    /// a public `group_info_message` containing an
650    /// [ExternalPubExt](crate::extension::ExternalPubExt), which can be
651    /// generated by an existing group member using the
652    /// [group_info_message](crate::group::Group::group_info_message)
653    /// function.
654    ///
655    /// `tree_data` may be provided following the same rules as [Client::join_group]
656    ///
657    /// If PSKs are provided in `external_psks`, the
658    /// [PreSharedKeyStorage](crate::PreSharedKeyStorage)
659    /// used to configure the client will be searched to resolve their values.
660    ///
661    /// `to_remove` may be used to remove an existing member provided that the
662    /// identity of the existing group member at that [index](crate::group::Member::index)
663    /// is a [valid successor](crate::IdentityProvider::valid_successor)
664    /// of `signing_identity` as defined by the
665    /// [IdentityProvider](crate::IdentityProvider) that this client
666    /// was configured with.
667    ///
668    /// # Warning
669    ///
670    /// Only one external commit can be performed against a given group info.
671    /// There may also be security trade-offs to this approach.
672    ///
673    // TODO: Add a comment about forward secrecy and a pointer to the future
674    // book chapter on this topic
675    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
676    pub async fn commit_external(
677        &self,
678        group_info_msg: MlsMessage,
679    ) -> Result<(Group<C>, MlsMessage), MlsError> {
680        ExternalCommitBuilder::new(
681            self.signer()?.clone(),
682            self.signing_identity()?.0.clone(),
683            self.config.clone(),
684        )
685        .build(group_info_msg)
686        .await
687    }
688
689    pub fn external_commit_builder(&self) -> Result<ExternalCommitBuilder<C>, MlsError> {
690        Ok(ExternalCommitBuilder::new(
691            self.signer()?.clone(),
692            self.signing_identity()?.0.clone(),
693            self.config.clone(),
694        ))
695    }
696
697    /// Load an existing group state into this client using the
698    /// [GroupStateStorage](crate::GroupStateStorage) that
699    /// this client was configured to use.
700    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
701    #[inline(never)]
702    pub async fn load_group(&self, group_id: &[u8]) -> Result<Group<C>, MlsError> {
703        let snapshot = self
704            .config
705            .group_state_storage()
706            .state(group_id)
707            .await
708            .map_err(|e| MlsError::GroupStorageError(e.into_any_error()))?
709            .ok_or(MlsError::GroupNotFound)?;
710
711        let snapshot = Snapshot::mls_decode(&mut &**snapshot)?;
712
713        Group::from_snapshot(self.config.clone(), snapshot).await
714    }
715
716    /// Load an existing group state into this client using the
717    /// [GroupStateStorage](crate::GroupStateStorage) that
718    /// this client was configured to use. The tree is taken from
719    /// `tree_data` instead of the stored state.
720    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
721    #[inline(never)]
722    pub async fn load_group_with_ratchet_tree(
723        &self,
724        group_id: &[u8],
725        tree_data: ExportedTree<'_>,
726    ) -> Result<Group<C>, MlsError> {
727        let snapshot = self
728            .config
729            .group_state_storage()
730            .state(group_id)
731            .await
732            .map_err(|e| MlsError::GroupStorageError(e.into_any_error()))?
733            .ok_or(MlsError::GroupNotFound)?;
734
735        let mut snapshot = Snapshot::mls_decode(&mut &**snapshot)?;
736        snapshot.state.public_tree.nodes = tree_data.0.into_owned();
737
738        Group::from_snapshot(self.config.clone(), snapshot).await
739    }
740
741    /// Request to join an existing [group](crate::group::Group).
742    ///
743    /// An existing group member will need to perform a
744    /// [commit](crate::Group::commit) to complete the add and the resulting
745    /// welcome message can be used by [join_group](Client::join_group).
746    #[cfg(feature = "by_ref_proposal")]
747    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
748    pub async fn external_add_proposal(
749        &self,
750        group_info: &MlsMessage,
751        tree_data: Option<crate::group::ExportedTree<'_>>,
752        authenticated_data: Vec<u8>,
753        key_package_extensions: ExtensionList,
754        leaf_node_extensions: ExtensionList,
755        timestamp: Option<MlsTime>,
756    ) -> Result<MlsMessage, MlsError> {
757        let protocol_version = group_info.version;
758
759        let protocol_version_ok =
760            self.config.version_supported(protocol_version) && protocol_version == self.version;
761
762        if !protocol_version_ok {
763            return Err(MlsError::UnsupportedProtocolVersion(protocol_version));
764        }
765
766        let group_info = group_info
767            .as_group_info()
768            .ok_or(MlsError::UnexpectedMessageType)?;
769
770        let cipher_suite = group_info.group_context.cipher_suite;
771
772        let cipher_suite_provider = self
773            .config
774            .crypto_provider()
775            .cipher_suite_provider(cipher_suite)
776            .ok_or(MlsError::UnsupportedCipherSuite(cipher_suite))?;
777
778        crate::group::validate_tree_and_info_joiner(
779            protocol_version,
780            group_info,
781            tree_data,
782            &self.config.identity_provider(),
783            &cipher_suite_provider,
784            timestamp,
785        )
786        .await?;
787
788        let key_package = self
789            .generate_key_package(key_package_extensions, leaf_node_extensions, timestamp)
790            .await?
791            .key_package;
792
793        (key_package.cipher_suite == cipher_suite)
794            .then_some(())
795            .ok_or(MlsError::UnsupportedCipherSuite(cipher_suite))?;
796
797        let message = AuthenticatedContent::new_signed(
798            &cipher_suite_provider,
799            &group_info.group_context,
800            Sender::NewMemberProposal,
801            Content::Proposal(Box::new(Proposal::Add(Box::new(AddProposal {
802                key_package,
803            })))),
804            self.signer()?,
805            WireFormat::PublicMessage,
806            authenticated_data,
807        )
808        .await?;
809
810        let plaintext = PublicMessage {
811            content: message.content,
812            auth: message.auth,
813            membership_tag: None,
814        };
815
816        Ok(MlsMessage {
817            version: protocol_version,
818            payload: MlsMessagePayload::Plain(plaintext),
819        })
820    }
821
822    fn signer(&self) -> Result<&SignatureSecretKey, MlsError> {
823        self.signer.as_ref().ok_or(MlsError::SignerNotFound)
824    }
825
826    pub fn signing_identity(&self) -> Result<(&SigningIdentity, CipherSuite), MlsError> {
827        self.signing_identity
828            .as_ref()
829            .map(|(id, cs)| (id, *cs))
830            .ok_or(MlsError::SignerNotFound)
831    }
832
833    /// The [KeyPackageStorage] that this client was configured to use.
834    pub fn key_package_store(&self) -> <C as ClientConfig>::KeyPackageRepository {
835        self.config.key_package_repo()
836    }
837
838    /// The [PreSharedKeyStorage](crate::PreSharedKeyStorage) that
839    /// this client was configured to use.
840    pub fn secret_store(&self) -> <C as ClientConfig>::PskStore {
841        self.config.secret_store()
842    }
843
844    /// The [GroupStateStorage] that this client was configured to use.
845    pub fn group_state_storage(&self) -> <C as ClientConfig>::GroupStateStorage {
846        self.config.group_state_storage()
847    }
848
849    /// The [IdentityProvider](crate::IdentityProvider) that this client was configured to use.
850    pub fn identity_provider(&self) -> <C as ClientConfig>::IdentityProvider {
851        self.config.identity_provider()
852    }
853}
854
855#[cfg(test)]
856pub(crate) mod test_utils {
857    use super::*;
858    use crate::identity::test_utils::get_test_signing_identity;
859
860    pub use crate::client_builder::test_utils::{TestClientBuilder, TestClientConfig};
861
862    pub const TEST_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::MLS_10;
863    pub const TEST_CIPHER_SUITE: CipherSuite = CipherSuite::P256_AES128;
864    pub const TEST_CUSTOM_PROPOSAL_TYPE: ProposalType = ProposalType::new(65001);
865
866    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
867    pub async fn test_client_with_key_pkg(
868        protocol_version: ProtocolVersion,
869        cipher_suite: CipherSuite,
870        identity: &str,
871    ) -> (Client<TestClientConfig>, MlsMessage) {
872        test_client_with_key_pkg_custom(
873            protocol_version,
874            cipher_suite,
875            identity,
876            Default::default(),
877            Default::default(),
878            |_| {},
879        )
880        .await
881    }
882
883    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
884    pub async fn test_client_with_key_pkg_custom<F>(
885        protocol_version: ProtocolVersion,
886        cipher_suite: CipherSuite,
887        identity: &str,
888        key_package_extensions: ExtensionList,
889        leaf_node_extensions: ExtensionList,
890        mut config: F,
891    ) -> (Client<TestClientConfig>, MlsMessage)
892    where
893        F: FnMut(&mut TestClientConfig),
894    {
895        let (identity, secret_key) =
896            get_test_signing_identity(cipher_suite, identity.as_bytes()).await;
897
898        let mut client = TestClientBuilder::new_for_test()
899            .used_protocol_version(protocol_version)
900            .signing_identity(identity.clone(), secret_key, cipher_suite)
901            .build();
902
903        config(&mut client.config);
904
905        let key_package = client
906            .generate_key_package_message(key_package_extensions, leaf_node_extensions, None)
907            .await
908            .unwrap();
909
910        (client, key_package)
911    }
912}
913
914#[cfg(test)]
915mod tests {
916    use super::test_utils::*;
917
918    use super::*;
919    use crate::{
920        crypto::test_utils::TestCryptoProvider,
921        identity::test_utils::{get_test_basic_credential, get_test_signing_identity},
922        tree_kem::leaf_node::LeafNodeSource,
923    };
924    use assert_matches::assert_matches;
925
926    #[cfg(feature = "by_ref_proposal")]
927    use crate::group::message_processor::ProposalMessageDescription;
928    #[cfg(feature = "by_ref_proposal")]
929    use crate::group::proposal::Proposal;
930    use crate::group::test_utils::test_group;
931    #[cfg(feature = "psk")]
932    use crate::group::test_utils::test_group_custom_config;
933    #[cfg(feature = "by_ref_proposal")]
934    use crate::group::ReceivedMessage;
935    #[cfg(feature = "psk")]
936    use crate::psk::{ExternalPskId, PreSharedKey};
937    use alloc::vec;
938
939    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
940    async fn test_keygen() {
941        // This is meant to test the inputs to the internal key package generator
942        // See KeyPackageGenerator tests for key generation specific tests
943        for (protocol_version, cipher_suite) in ProtocolVersion::all().flat_map(|p| {
944            TestCryptoProvider::all_supported_cipher_suites()
945                .into_iter()
946                .map(move |cs| (p, cs))
947        }) {
948            let (identity, secret_key) = get_test_signing_identity(cipher_suite, b"foo").await;
949
950            let client = TestClientBuilder::new_for_test()
951                .signing_identity(identity.clone(), secret_key, cipher_suite)
952                .build();
953
954            // TODO: Tests around extensions
955            let key_package = client
956                .generate_key_package_message(Default::default(), Default::default(), None)
957                .await
958                .unwrap();
959
960            assert_eq!(key_package.version, protocol_version);
961
962            let key_package = key_package.into_key_package().unwrap();
963
964            assert_eq!(key_package.cipher_suite, cipher_suite);
965
966            assert_eq!(
967                &key_package.leaf_node.signing_identity.credential,
968                &get_test_basic_credential(b"foo".to_vec())
969            );
970
971            assert_eq!(key_package.leaf_node.signing_identity, identity);
972
973            let capabilities = key_package.leaf_node.ungreased_capabilities();
974            assert_eq!(capabilities, client.config.capabilities());
975
976            let client_lifetime = client.config.lifetime(None);
977            assert_matches!(key_package.leaf_node.leaf_node_source, LeafNodeSource::KeyPackage(lifetime) if (lifetime.not_after - lifetime.not_before) == (client_lifetime.not_after - client_lifetime.not_before));
978        }
979    }
980
981    #[cfg(feature = "by_ref_proposal")]
982    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
983    async fn new_member_add_proposal_adds_to_group() {
984        let mut alice_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
985
986        let (bob_identity, secret_key) = get_test_signing_identity(TEST_CIPHER_SUITE, b"bob").await;
987
988        let bob = TestClientBuilder::new_for_test()
989            .signing_identity(bob_identity.clone(), secret_key, TEST_CIPHER_SUITE)
990            .build();
991
992        let proposal = bob
993            .external_add_proposal(
994                &alice_group.group_info_message(true).await.unwrap(),
995                None,
996                vec![],
997                Default::default(),
998                Default::default(),
999                None,
1000            )
1001            .await
1002            .unwrap();
1003
1004        let message = alice_group
1005            .process_incoming_message(proposal)
1006            .await
1007            .unwrap();
1008
1009        assert_matches!(
1010            message,
1011            ReceivedMessage::Proposal(ProposalMessageDescription {
1012                proposal: Proposal::Add(p), ..}
1013            ) if p.key_package.leaf_node.signing_identity == bob_identity
1014        );
1015
1016        alice_group.commit(vec![]).await.unwrap();
1017        alice_group.apply_pending_commit().await.unwrap();
1018
1019        // Check that the new member is in the group
1020        assert!(alice_group
1021            .roster()
1022            .members_iter()
1023            .any(|member| member.signing_identity == bob_identity))
1024    }
1025
1026    #[cfg(feature = "psk")]
1027    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
1028    async fn join_via_external_commit(do_remove: bool, with_psk: bool) -> Result<(), MlsError> {
1029        // An external commit cannot be the first commit in a group as it requires
1030        // interim_transcript_hash to be computed from the confirmed_transcript_hash and
1031        // confirmation_tag, which is not the case for the initial interim_transcript_hash.
1032
1033        use crate::group::{message_processor::CommitEffect, CommitMessageDescription};
1034
1035        let psk = PreSharedKey::from(b"psk".to_vec());
1036        let psk_id = ExternalPskId::new(b"psk id".to_vec());
1037
1038        let mut alice_group =
1039            test_group_custom_config(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, |c| {
1040                c.psk(psk_id.clone(), psk.clone())
1041            })
1042            .await;
1043
1044        let (mut bob_group, _) = alice_group
1045            .join_with_custom_config("bob", false, |c| {
1046                c.0.psk_store.insert(psk_id.clone(), psk.clone());
1047            })
1048            .await
1049            .unwrap();
1050
1051        let group_info_msg = alice_group
1052            .group_info_message_allowing_ext_commit(true)
1053            .await
1054            .unwrap();
1055
1056        let new_client_id = if do_remove { "bob" } else { "charlie" };
1057
1058        let (new_client_identity, secret_key) =
1059            get_test_signing_identity(TEST_CIPHER_SUITE, new_client_id.as_bytes()).await;
1060
1061        let new_client = TestClientBuilder::new_for_test()
1062            .psk(psk_id.clone(), psk)
1063            .signing_identity(new_client_identity.clone(), secret_key, TEST_CIPHER_SUITE)
1064            .build();
1065
1066        let mut builder = new_client.external_commit_builder().unwrap();
1067
1068        if do_remove {
1069            builder = builder.with_removal(1);
1070        }
1071
1072        if with_psk {
1073            builder = builder.with_external_psk(psk_id);
1074        }
1075
1076        let (new_group, external_commit) = builder.build(group_info_msg).await?;
1077
1078        let num_members = if do_remove { 2 } else { 3 };
1079
1080        assert_eq!(new_group.roster().members_iter().count(), num_members);
1081
1082        let _ = alice_group
1083            .process_incoming_message(external_commit.clone())
1084            .await
1085            .unwrap();
1086
1087        let bob_current_epoch = bob_group.current_epoch();
1088
1089        let message = bob_group
1090            .process_incoming_message(external_commit)
1091            .await
1092            .unwrap();
1093
1094        assert!(alice_group.roster().members_iter().count() == num_members);
1095
1096        if !do_remove {
1097            assert!(bob_group.roster().members_iter().count() == num_members);
1098        } else {
1099            // Bob was removed so his epoch must stay the same
1100            assert_eq!(bob_group.current_epoch(), bob_current_epoch);
1101
1102            assert_matches!(
1103                message,
1104                ReceivedMessage::Commit(CommitMessageDescription {
1105                    effect: CommitEffect::Removed {
1106                        new_epoch: _,
1107                        remover: _
1108                    },
1109                    ..
1110                })
1111            );
1112        }
1113
1114        // Comparing epoch authenticators is sufficient to check that members are in sync.
1115        assert_eq!(
1116            alice_group.epoch_authenticator().unwrap(),
1117            new_group.epoch_authenticator().unwrap()
1118        );
1119
1120        Ok(())
1121    }
1122
1123    #[cfg(feature = "psk")]
1124    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1125    async fn test_external_commit() {
1126        // New member can join
1127        join_via_external_commit(false, false).await.unwrap();
1128        // New member can remove an old copy of themselves
1129        join_via_external_commit(true, false).await.unwrap();
1130        // New member can inject a PSK
1131        join_via_external_commit(false, true).await.unwrap();
1132        // All works together
1133        join_via_external_commit(true, true).await.unwrap();
1134    }
1135
1136    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1137    async fn creating_an_external_commit_requires_a_group_info_message() {
1138        let (alice_identity, secret_key) =
1139            get_test_signing_identity(TEST_CIPHER_SUITE, b"alice").await;
1140
1141        let alice = TestClientBuilder::new_for_test()
1142            .signing_identity(alice_identity.clone(), secret_key, TEST_CIPHER_SUITE)
1143            .build();
1144
1145        let msg = alice
1146            .generate_key_package_message(Default::default(), Default::default(), None)
1147            .await
1148            .unwrap();
1149        let res = alice.commit_external(msg).await.map(|_| ());
1150
1151        assert_matches!(res, Err(MlsError::UnexpectedMessageType));
1152    }
1153
1154    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1155    async fn external_commit_with_invalid_group_info_fails() {
1156        let mut alice_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1157        let mut bob_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1158
1159        bob_group.commit(vec![]).await.unwrap();
1160        bob_group.apply_pending_commit().await.unwrap();
1161
1162        let group_info_msg = bob_group
1163            .group_info_message_allowing_ext_commit(true)
1164            .await
1165            .unwrap();
1166
1167        let (carol_identity, secret_key) =
1168            get_test_signing_identity(TEST_CIPHER_SUITE, b"carol").await;
1169
1170        let carol = TestClientBuilder::new_for_test()
1171            .signing_identity(carol_identity, secret_key, TEST_CIPHER_SUITE)
1172            .build();
1173
1174        let (_, external_commit) = carol
1175            .external_commit_builder()
1176            .unwrap()
1177            .build(group_info_msg)
1178            .await
1179            .unwrap();
1180
1181        // If Carol tries to join Alice's group using the group info from Bob's group, that fails.
1182        let res = alice_group.process_incoming_message(external_commit).await;
1183        assert_matches!(res, Err(_));
1184    }
1185
1186    #[test]
1187    fn builder_can_be_obtained_from_client_to_edit_properties_for_new_client() {
1188        let alice = TestClientBuilder::new_for_test()
1189            .extension_type(33.into())
1190            .build();
1191        let bob = alice.to_builder(None).extension_type(34.into()).build();
1192        assert_eq!(bob.config.supported_extensions(), [33, 34].map(Into::into));
1193    }
1194
1195    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1196    async fn examine_welcome_message() {
1197        let mut alice = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE)
1198            .await
1199            .group;
1200
1201        let (bob, kp) =
1202            test_client_with_key_pkg(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "bob").await;
1203
1204        let commit = alice
1205            .commit_builder()
1206            .add_member(kp)
1207            .unwrap()
1208            .build()
1209            .await
1210            .unwrap();
1211
1212        alice.apply_pending_commit().await.unwrap();
1213
1214        let mut group_info = bob
1215            .examine_welcome_message(&commit.welcome_messages[0])
1216            .await
1217            .unwrap();
1218
1219        // signature is random so we won't compare it
1220        group_info.signature = vec![];
1221        group_info.ungrease();
1222
1223        let mut expected_group_info = alice
1224            .group_info_message(commit.ratchet_tree.is_none())
1225            .await
1226            .unwrap()
1227            .into_group_info()
1228            .unwrap();
1229
1230        expected_group_info.signature = vec![];
1231        expected_group_info.ungrease();
1232
1233        assert_eq!(expected_group_info, group_info);
1234    }
1235
1236    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1237    async fn validate_group_info() {
1238        let alice = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE)
1239            .await
1240            .group;
1241
1242        let bob = test_client_with_key_pkg(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "bob")
1243            .await
1244            .0;
1245
1246        let group_info = alice.group_info_message(false).await.unwrap();
1247        let alice_signer = alice.current_member_signing_identity().unwrap().clone();
1248
1249        bob.validate_group_info(&group_info, &alice_signer)
1250            .await
1251            .unwrap();
1252
1253        let other_signer = get_test_signing_identity(TEST_CIPHER_SUITE, b"alice")
1254            .await
1255            .0;
1256
1257        let res = bob.validate_group_info(&group_info, &other_signer).await;
1258        assert_matches!(res, Err(MlsError::InvalidSignature));
1259    }
1260
1261    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1262
1263    async fn cannot_list_default_extensions_in_capabilities() {
1264        let res = TestClientBuilder::new_for_test()
1265            .with_random_signing_identity("client", TEST_CIPHER_SUITE)
1266            .await
1267            .extension_type(ExtensionType::APPLICATION_ID)
1268            .build()
1269            .generate_key_package(Default::default(), Default::default(), Default::default())
1270            .await;
1271
1272        assert_matches!(res, Err(MlsError::DefaultValueListed));
1273    }
1274
1275    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
1276
1277    async fn cannot_list_default_proposals_in_capabilities() {
1278        let res = TestClientBuilder::new_for_test()
1279            .with_random_signing_identity("client", TEST_CIPHER_SUITE)
1280            .await
1281            .custom_proposal_type(ProposalType::ADD)
1282            .build()
1283            .generate_key_package(Default::default(), Default::default(), Default::default())
1284            .await;
1285
1286        assert_matches!(res, Err(MlsError::DefaultValueListed));
1287    }
1288}