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