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("Invalid commit, missing required path"))]
63    CommitMissingPath,
64    #[cfg_attr(feature = "std", error("plaintext message for incorrect epoch"))]
65    InvalidEpoch,
66    #[cfg_attr(feature = "std", error("invalid signature found"))]
67    InvalidSignature,
68    #[cfg_attr(feature = "std", error("invalid confirmation tag"))]
69    InvalidConfirmationTag,
70    #[cfg_attr(feature = "std", error("invalid membership tag"))]
71    InvalidMembershipTag,
72    #[cfg_attr(feature = "std", error("corrupt private key, missing required values"))]
73    InvalidTreeKemPrivateKey,
74    #[cfg_attr(feature = "std", error("key package not found, unable to process"))]
75    WelcomeKeyPackageNotFound,
76    #[cfg_attr(feature = "std", error("leaf not found in tree for index {0}"))]
77    LeafNotFound(u32),
78    #[cfg_attr(feature = "std", error("message from self can't be processed"))]
79    CantProcessMessageFromSelf,
80    #[cfg_attr(
81        feature = "std",
82        error("pending proposals found, commit required before application messages can be sent")
83    )]
84    CommitRequired,
85    #[cfg_attr(
86        feature = "std",
87        error("ratchet tree not provided or discovered in GroupInfo")
88    )]
89    RatchetTreeNotFound,
90    #[cfg_attr(feature = "std", error("External sender cannot commit"))]
91    ExternalSenderCannotCommit,
92    #[cfg_attr(feature = "std", error("Unsupported protocol version {0:?}"))]
93    UnsupportedProtocolVersion(ProtocolVersion),
94    #[cfg_attr(feature = "std", error("Protocol version mismatch"))]
95    ProtocolVersionMismatch,
96    #[cfg_attr(feature = "std", error("Unsupported cipher suite {0:?}"))]
97    UnsupportedCipherSuite(CipherSuite),
98    #[cfg_attr(feature = "std", error("Signing key of external sender is unknown"))]
99    UnknownSigningIdentityForExternalSender,
100    #[cfg_attr(
101        feature = "std",
102        error("External proposals are disabled for this group")
103    )]
104    ExternalProposalsDisabled,
105    #[cfg_attr(
106        feature = "std",
107        error("Signing identity is not allowed to externally propose")
108    )]
109    InvalidExternalSigningIdentity,
110    #[cfg_attr(feature = "std", error("Missing ExternalPub extension"))]
111    MissingExternalPubExtension,
112    #[cfg_attr(feature = "std", error("Epoch not found"))]
113    EpochNotFound,
114    #[cfg_attr(feature = "std", error("Unencrypted application message"))]
115    UnencryptedApplicationMessage,
116    #[cfg_attr(
117        feature = "std",
118        error("NewMemberCommit sender type can only be used to send Commit content")
119    )]
120    ExpectedCommitForNewMemberCommit,
121    #[cfg_attr(
122        feature = "std",
123        error("NewMemberProposal sender type can only be used to send add proposals")
124    )]
125    ExpectedAddProposalForNewMemberProposal,
126    #[cfg_attr(
127        feature = "std",
128        error("External commit missing ExternalInit proposal")
129    )]
130    ExternalCommitMissingExternalInit,
131    #[cfg_attr(
132        feature = "std",
133        error(
134            "A ReIinit has been applied. The next action must be creating or receiving a welcome."
135        )
136    )]
137    GroupUsedAfterReInit,
138    #[cfg_attr(feature = "std", error("Pending ReIinit not found."))]
139    PendingReInitNotFound,
140    #[cfg_attr(
141        feature = "std",
142        error("The extensions in the welcome message and in the reinit do not match.")
143    )]
144    ReInitExtensionsMismatch,
145    #[cfg_attr(feature = "std", error("signer not found for given identity"))]
146    SignerNotFound,
147    #[cfg_attr(feature = "std", error("commit already pending"))]
148    ExistingPendingCommit,
149    #[cfg_attr(feature = "std", error("pending commit not found"))]
150    PendingCommitNotFound,
151    #[cfg_attr(feature = "std", error("unexpected message type for action"))]
152    UnexpectedMessageType,
153    #[cfg_attr(
154        feature = "std",
155        error("membership tag on MlsPlaintext for non-member sender")
156    )]
157    MembershipTagForNonMember,
158    #[cfg_attr(feature = "std", error("No member found for given identity id."))]
159    MemberNotFound,
160    #[cfg_attr(feature = "std", error("group not found"))]
161    GroupNotFound,
162    #[cfg_attr(feature = "std", error("unexpected PSK ID"))]
163    UnexpectedPskId,
164    #[cfg_attr(feature = "std", error("invalid sender for content type"))]
165    InvalidSender,
166    #[cfg_attr(feature = "std", error("GroupID mismatch"))]
167    GroupIdMismatch,
168    #[cfg_attr(feature = "std", error("storage retention can not be zero"))]
169    NonZeroRetentionRequired,
170    #[cfg_attr(feature = "std", error("Too many PSK IDs to compute PSK secret"))]
171    TooManyPskIds,
172    #[cfg_attr(feature = "std", error("Missing required Psk"))]
173    MissingRequiredPsk,
174    #[cfg_attr(feature = "std", error("Old group state not found"))]
175    OldGroupStateNotFound,
176    #[cfg_attr(feature = "std", error("leaf secret already consumed"))]
177    InvalidLeafConsumption,
178    #[cfg_attr(feature = "std", error("key not available, invalid generation {0}"))]
179    KeyMissing(u32),
180    #[cfg_attr(
181        feature = "std",
182        error("requested generation {0} is too far ahead of current generation")
183    )]
184    InvalidFutureGeneration(u32),
185    #[cfg_attr(feature = "std", error("leaf node has no children"))]
186    LeafNodeNoChildren,
187    #[cfg_attr(feature = "std", error("root node has no parent"))]
188    LeafNodeNoParent,
189    #[cfg_attr(feature = "std", error("index out of range"))]
190    InvalidTreeIndex,
191    #[cfg_attr(feature = "std", error("time overflow"))]
192    TimeOverflow,
193    #[cfg_attr(feature = "std", error("invalid leaf_node_source"))]
194    InvalidLeafNodeSource,
195    #[cfg_attr(
196        feature = "std",
197        error("current time ({}) is not within key package lifetime ({} to {})",
198              timestamp.seconds_since_epoch(),
199              not_before.seconds_since_epoch(),
200              not_after.seconds_since_epoch(),
201        )
202    )]
203    InvalidLifetime {
204        not_before: MlsTime,
205        not_after: MlsTime,
206        timestamp: MlsTime,
207    },
208    #[cfg_attr(feature = "std", error("required extension not found"))]
209    RequiredExtensionNotFound(ExtensionType),
210    #[cfg_attr(feature = "std", error("required proposal not found"))]
211    RequiredProposalNotFound(ProposalType),
212    #[cfg_attr(feature = "std", error("required credential not found"))]
213    RequiredCredentialNotFound(CredentialType),
214    #[cfg_attr(feature = "std", error("capabilities must describe extensions used"))]
215    ExtensionNotInCapabilities(ExtensionType),
216    #[cfg_attr(feature = "std", error("expected non-blank node"))]
217    ExpectedNode,
218    #[cfg_attr(feature = "std", error("node index is out of bounds {0}"))]
219    InvalidNodeIndex(NodeIndex),
220    #[cfg_attr(feature = "std", error("unexpected empty node found"))]
221    UnexpectedEmptyNode,
222    #[cfg_attr(
223        feature = "std",
224        error("duplicate signature key, hpke key or identity found at index {0}")
225    )]
226    DuplicateLeafData(u32),
227    #[cfg_attr(
228        feature = "std",
229        error("In-use credential type not supported by new leaf at index")
230    )]
231    InUseCredentialTypeUnsupportedByNewLeaf,
232    #[cfg_attr(
233        feature = "std",
234        error("Not all members support the credential type used by new leaf")
235    )]
236    CredentialTypeOfNewLeafIsUnsupported,
237    #[cfg_attr(
238        feature = "std",
239        error("the length of the update path is different than the length of the direct path")
240    )]
241    WrongPathLen,
242    #[cfg_attr(
243        feature = "std",
244        error("same HPKE leaf key before and after applying the update path for leaf {0}")
245    )]
246    SameHpkeKey(u32),
247    #[cfg_attr(feature = "std", error("init key is not valid for cipher suite"))]
248    InvalidInitKey,
249    #[cfg_attr(
250        feature = "std",
251        error("init key can not be equal to leaf node public key")
252    )]
253    InitLeafKeyEquality,
254    #[cfg_attr(feature = "std", error("different identity in update for leaf {0}"))]
255    DifferentIdentityInUpdate(u32),
256    #[cfg_attr(feature = "std", error("update path pub key mismatch"))]
257    PubKeyMismatch,
258    #[cfg_attr(feature = "std", error("tree hash mismatch"))]
259    TreeHashMismatch,
260    #[cfg_attr(feature = "std", error("bad update: no suitable secret key"))]
261    UpdateErrorNoSecretKey,
262    #[cfg_attr(feature = "std", error("invalid lca, not found on direct path"))]
263    LcaNotFoundInDirectPath,
264    #[cfg_attr(feature = "std", error("update path parent hash mismatch"))]
265    ParentHashMismatch,
266    #[cfg_attr(feature = "std", error("unexpected pattern of unmerged leaves"))]
267    UnmergedLeavesMismatch,
268    #[cfg_attr(feature = "std", error("empty tree"))]
269    UnexpectedEmptyTree,
270    #[cfg_attr(feature = "std", error("trailing blanks"))]
271    UnexpectedTrailingBlanks,
272    // Proposal Rules errors
273    #[cfg_attr(
274        feature = "std",
275        error("Commiter must not include any update proposals generated by the commiter")
276    )]
277    InvalidCommitSelfUpdate,
278    #[cfg_attr(feature = "std", error("A PreSharedKey proposal must have a PSK of type External or type Resumption and usage Application"))]
279    InvalidTypeOrUsageInPreSharedKeyProposal,
280    #[cfg_attr(feature = "std", error("psk nonce length does not match cipher suite"))]
281    InvalidPskNonceLength,
282    #[cfg_attr(
283        feature = "std",
284        error("ReInit proposal protocol version is less than the version of the original group")
285    )]
286    InvalidProtocolVersionInReInit,
287    #[cfg_attr(feature = "std", error("More than one proposal applying to leaf: {0}"))]
288    MoreThanOneProposalForLeaf(u32),
289    #[cfg_attr(
290        feature = "std",
291        error("More than one GroupContextExtensions proposal")
292    )]
293    MoreThanOneGroupContextExtensionsProposal,
294    #[cfg_attr(feature = "std", error("Invalid proposal type for sender"))]
295    InvalidProposalTypeForSender,
296    #[cfg_attr(
297        feature = "std",
298        error("External commit must have exactly one ExternalInit proposal")
299    )]
300    ExternalCommitMustHaveExactlyOneExternalInit,
301    #[cfg_attr(feature = "std", error("External commit must have a new leaf"))]
302    ExternalCommitMustHaveNewLeaf,
303    #[cfg_attr(
304        feature = "std",
305        error("External commit contains removal of other identity")
306    )]
307    ExternalCommitRemovesOtherIdentity,
308    #[cfg_attr(
309        feature = "std",
310        error("External commit contains more than one Remove proposal")
311    )]
312    ExternalCommitWithMoreThanOneRemove,
313    #[cfg_attr(feature = "std", error("Duplicate PSK IDs"))]
314    DuplicatePskIds,
315    #[cfg_attr(
316        feature = "std",
317        error("Invalid proposal type {0:?} in external commit")
318    )]
319    InvalidProposalTypeInExternalCommit(ProposalType),
320    #[cfg_attr(feature = "std", error("Committer can not remove themselves"))]
321    CommitterSelfRemoval,
322    #[cfg_attr(
323        feature = "std",
324        error("Only members can commit proposals by reference")
325    )]
326    OnlyMembersCanCommitProposalsByRef,
327    #[cfg_attr(feature = "std", error("Other proposal with ReInit"))]
328    OtherProposalWithReInit,
329    #[cfg_attr(feature = "std", error("Unsupported group extension {0:?}"))]
330    UnsupportedGroupExtension(ExtensionType),
331    #[cfg_attr(feature = "std", error("Unsupported custom proposal type {0:?}"))]
332    UnsupportedCustomProposal(ProposalType),
333    #[cfg_attr(feature = "std", error("by-ref proposal not found"))]
334    ProposalNotFound,
335    #[cfg_attr(
336        feature = "std",
337        error("Removing non-existing member (or removing a member twice)")
338    )]
339    RemovingNonExistingMember,
340    #[cfg_attr(feature = "std", error("Updated identity not a valid successor"))]
341    InvalidSuccessor,
342    #[cfg_attr(
343        feature = "std",
344        error("Updating non-existing member (or updating a member twice)")
345    )]
346    UpdatingNonExistingMember,
347    #[cfg_attr(feature = "std", error("Failed generating next path secret"))]
348    FailedGeneratingPathSecret,
349    #[cfg_attr(feature = "std", error("Invalid group info"))]
350    InvalidGroupInfo,
351    #[cfg_attr(feature = "std", error("Invalid welcome message"))]
352    InvalidWelcomeMessage,
353    #[cfg_attr(feature = "std", error("Exporter deleted"))]
354    ExporterDeleted,
355    #[cfg_attr(feature = "std", error("Self-remove already proposed"))]
356    SelfRemoveAlreadyProposed,
357}
358
359impl IntoAnyError for MlsError {
360    #[cfg(feature = "std")]
361    fn into_dyn_error(self) -> Result<Box<dyn std::error::Error + Send + Sync>, Self> {
362        Ok(self.into())
363    }
364}
365
366impl From<mls_rs_codec::Error> for MlsError {
367    #[inline]
368    fn from(e: mls_rs_codec::Error) -> Self {
369        MlsError::SerializationError(e.into_any_error())
370    }
371}
372
373impl From<ExtensionError> for MlsError {
374    #[inline]
375    fn from(e: ExtensionError) -> Self {
376        MlsError::ExtensionError(e.into_any_error())
377    }
378}
379
380/// MLS client used to create key packages and manage groups.
381///
382/// [`Client::builder`] can be used to instantiate it.
383///
384/// Clients are able to support multiple protocol versions, ciphersuites
385/// and underlying identities used to join groups and generate key packages.
386/// Applications may decide to create one or many clients depending on their
387/// specific needs.
388#[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::ffi_type(opaque))]
389#[derive(Clone, Debug)]
390pub struct Client<C> {
391    pub(crate) config: C,
392    pub(crate) signing_identity: Option<(SigningIdentity, CipherSuite)>,
393    pub(crate) signer: Option<SignatureSecretKey>,
394    pub(crate) version: ProtocolVersion,
395}
396
397impl Client<()> {
398    /// Returns a [`ClientBuilder`]
399    /// used to configure client preferences and providers.
400    pub fn builder() -> ClientBuilder<BaseConfig> {
401        ClientBuilder::new()
402    }
403}
404
405#[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen)]
406impl<C> Client<C>
407where
408    C: ClientConfig + Clone,
409{
410    pub(crate) fn new(
411        config: C,
412        signer: Option<SignatureSecretKey>,
413        signing_identity: Option<(SigningIdentity, CipherSuite)>,
414        version: ProtocolVersion,
415    ) -> Self {
416        Client {
417            config,
418            signer,
419            signing_identity,
420            version,
421        }
422    }
423
424    #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen_ignore)]
425    pub fn to_builder(&self, timestamp: Option<MlsTime>) -> ClientBuilder<MakeConfig<C>> {
426        ClientBuilder::from_config(recreate_config(
427            self.config.clone(),
428            self.signer.clone(),
429            self.signing_identity.clone(),
430            self.version,
431            timestamp,
432        ))
433    }
434
435    /// Creates a new key package message that can be used to to add this
436    /// client to a [Group](crate::group::Group). Each call to this function
437    /// will produce a unique value that is signed by `signing_identity`.
438    ///
439    /// The secret keys for the resulting key package message will be stored in
440    /// the [KeyPackageStorage](crate::KeyPackageStorage)
441    /// that was used to configure the client and will
442    /// automatically be erased when this key package is used to
443    /// [join a group](Client::join_group).
444    ///
445    /// # Warning
446    ///
447    /// A key package message may only be used once.
448    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
449    #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen_ignore)]
450    pub async fn generate_key_package_message(
451        &self,
452        key_package_extensions: ExtensionList,
453        leaf_node_extensions: ExtensionList,
454        timestamp: Option<MlsTime>,
455    ) -> Result<MlsMessage, MlsError> {
456        Ok(self
457            .generate_key_package(key_package_extensions, leaf_node_extensions, timestamp)
458            .await?
459            .key_package_message())
460    }
461
462    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
463    async fn generate_key_package(
464        &self,
465        key_package_extensions: ExtensionList,
466        leaf_node_extensions: ExtensionList,
467        timestamp: Option<MlsTime>,
468    ) -> Result<KeyPackageGeneration, MlsError> {
469        let (signing_identity, cipher_suite) = self.signing_identity()?;
470
471        let cipher_suite_provider = self
472            .config
473            .crypto_provider()
474            .cipher_suite_provider(cipher_suite)
475            .ok_or(MlsError::UnsupportedCipherSuite(cipher_suite))?;
476
477        let key_package_generator = KeyPackageGenerator {
478            protocol_version: self.version,
479            cipher_suite_provider: &cipher_suite_provider,
480            signing_key: self.signer()?,
481            signing_identity,
482        };
483
484        let key_pkg_gen = key_package_generator
485            .generate(
486                self.config.lifetime(timestamp),
487                self.config.capabilities(),
488                key_package_extensions,
489                leaf_node_extensions,
490            )
491            .await?;
492
493        let (id, key_package_data) = key_pkg_gen.to_storage()?;
494
495        self.config
496            .key_package_repo()
497            .insert(id, key_package_data)
498            .await
499            .map_err(|e| MlsError::KeyPackageRepoError(e.into_any_error()))?;
500
501        Ok(key_pkg_gen)
502    }
503
504    /// Create a group with a specific group_id.
505    ///
506    /// This function behaves the same way as
507    /// [create_group](Client::create_group) except that it
508    /// specifies a specific unique group identifier to be used.
509    ///
510    /// # Warning
511    ///
512    /// It is recommended to use [create_group](Client::create_group)
513    /// instead of this function because it guarantees that group_id values
514    /// are globally unique.
515    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
516    #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen_ignore)]
517    pub async fn create_group_with_id(
518        &self,
519        group_id: Vec<u8>,
520        group_context_extensions: ExtensionList,
521        leaf_node_extensions: ExtensionList,
522        timestamp: Option<MlsTime>,
523    ) -> Result<Group<C>, MlsError> {
524        let (signing_identity, cipher_suite) = self.signing_identity()?;
525
526        Group::new(
527            self.config.clone(),
528            Some(group_id),
529            cipher_suite,
530            self.version,
531            signing_identity.clone(),
532            group_context_extensions,
533            leaf_node_extensions,
534            self.signer()?.clone(),
535            timestamp,
536        )
537        .await
538    }
539
540    /// Create a MLS group.
541    ///
542    /// The `cipher_suite` provided must be supported by the
543    /// [CipherSuiteProvider](crate::CipherSuiteProvider)
544    /// that was used to build the client.
545    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
546    #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen_ignore)]
547    pub async fn create_group(
548        &self,
549        group_context_extensions: ExtensionList,
550        leaf_node_extensions: ExtensionList,
551        timestamp: Option<MlsTime>,
552    ) -> Result<Group<C>, MlsError> {
553        let (signing_identity, cipher_suite) = self.signing_identity()?;
554
555        Group::new(
556            self.config.clone(),
557            None,
558            cipher_suite,
559            self.version,
560            signing_identity.clone(),
561            group_context_extensions,
562            leaf_node_extensions,
563            self.signer()?.clone(),
564            timestamp,
565        )
566        .await
567    }
568
569    /// Join a MLS group via a welcome message created by a
570    /// [Commit](crate::group::CommitOutput).
571    ///
572    /// `tree_data` is required to be provided out of band if the client that
573    /// created `welcome_message` did not use the `ratchet_tree_extension`
574    /// according to [`MlsRules::commit_options`](`crate::MlsRules::commit_options`).
575    /// at the time the welcome message was created. `tree_data` can
576    /// be exported from a group using the
577    /// [export tree function](crate::group::Group::export_tree).
578    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
579    #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen_ignore)]
580    pub async fn join_group(
581        &self,
582        tree_data: Option<ExportedTree<'_>>,
583        welcome_message: &MlsMessage,
584        maybe_time: Option<MlsTime>,
585    ) -> Result<(Group<C>, NewMemberInfo), MlsError> {
586        Group::join(
587            welcome_message,
588            tree_data,
589            self.config.clone(),
590            self.signer()?.clone(),
591            maybe_time,
592        )
593        .await
594    }
595
596    /// Decrypt GroupInfo encrypted in the Welcome message without actually joining
597    /// the group. The ratchet tree is not needed.
598    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
599    pub async fn examine_welcome_message(
600        &self,
601        welcome_message: &MlsMessage,
602    ) -> Result<GroupInfo, MlsError> {
603        Group::decrypt_group_info(welcome_message, &self.config).await
604    }
605
606    /// Validate GroupInfo message. This does NOT validate the ratchet tree in case
607    /// it is provided in the extension. It validates the signature, identity of the
608    /// signer, identities of external senders and cipher suite.
609    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
610    pub async fn validate_group_info(
611        &self,
612        group_info_message: &MlsMessage,
613        signer: &SigningIdentity,
614    ) -> Result<(), MlsError> {
615        let MlsMessagePayload::GroupInfo(group_info) = &group_info_message.payload else {
616            return Err(MlsError::UnexpectedMessageType);
617        };
618
619        let cs = cipher_suite_provider(
620            self.config.crypto_provider(),
621            group_info.group_context.cipher_suite,
622        )?;
623
624        let id = self.config.identity_provider();
625
626        validate_group_info_joiner(group_info_message.version, group_info, signer, &id, &cs)
627            .await?;
628
629        let context = MemberValidationContext::ForNewGroup {
630            current_context: &group_info.group_context,
631        };
632
633        id.validate_member(signer, None, context)
634            .await
635            .map_err(|e| MlsError::IdentityProviderError(e.into_any_error()))?;
636
637        Ok(())
638    }
639
640    /// 0-RTT add to an existing [group](crate::group::Group)
641    ///
642    /// External commits allow for immediate entry into a
643    /// [group](crate::group::Group), even if all of the group members
644    /// are currently offline and unable to process messages. Sending an
645    /// external commit is only allowed for groups that have provided
646    /// a public `group_info_message` containing an
647    /// [ExternalPubExt](crate::extension::ExternalPubExt), which can be
648    /// generated by an existing group member using the
649    /// [group_info_message](crate::group::Group::group_info_message)
650    /// function.
651    ///
652    /// `tree_data` may be provided following the same rules as [Client::join_group]
653    ///
654    /// If PSKs are provided in `external_psks`, the
655    /// [PreSharedKeyStorage](crate::PreSharedKeyStorage)
656    /// used to configure the client will be searched to resolve their values.
657    ///
658    /// `to_remove` may be used to remove an existing member provided that the
659    /// identity of the existing group member at that [index](crate::group::Member::index)
660    /// is a [valid successor](crate::IdentityProvider::valid_successor)
661    /// of `signing_identity` as defined by the
662    /// [IdentityProvider](crate::IdentityProvider) that this client
663    /// was configured with.
664    ///
665    /// # Warning
666    ///
667    /// Only one external commit can be performed against a given group info.
668    /// There may also be security trade-offs to this approach.
669    ///
670    // TODO: Add a comment about forward secrecy and a pointer to the future
671    // book chapter on this topic
672    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
673    pub async fn commit_external(
674        &self,
675        group_info_msg: MlsMessage,
676    ) -> Result<(Group<C>, MlsMessage), MlsError> {
677        ExternalCommitBuilder::new(
678            self.signer()?.clone(),
679            self.signing_identity()?.0.clone(),
680            self.config.clone(),
681        )
682        .build(group_info_msg)
683        .await
684    }
685
686    pub fn external_commit_builder(&self) -> Result<ExternalCommitBuilder<C>, MlsError> {
687        Ok(ExternalCommitBuilder::new(
688            self.signer()?.clone(),
689            self.signing_identity()?.0.clone(),
690            self.config.clone(),
691        ))
692    }
693
694    /// Load an existing group state into this client using the
695    /// [GroupStateStorage](crate::GroupStateStorage) that
696    /// this client was configured to use.
697    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
698    #[inline(never)]
699    pub async fn load_group(&self, group_id: &[u8]) -> Result<Group<C>, MlsError> {
700        let snapshot = self
701            .config
702            .group_state_storage()
703            .state(group_id)
704            .await
705            .map_err(|e| MlsError::GroupStorageError(e.into_any_error()))?
706            .ok_or(MlsError::GroupNotFound)?;
707
708        let snapshot = Snapshot::mls_decode(&mut &*snapshot)?;
709
710        Group::from_snapshot(self.config.clone(), snapshot).await
711    }
712
713    /// Load an existing group state into this client using the
714    /// [GroupStateStorage](crate::GroupStateStorage) that
715    /// this client was configured to use. The tree is taken from
716    /// `tree_data` instead of the stored state.
717    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
718    #[inline(never)]
719    pub async fn load_group_with_ratchet_tree(
720        &self,
721        group_id: &[u8],
722        tree_data: ExportedTree<'_>,
723    ) -> Result<Group<C>, MlsError> {
724        let snapshot = self
725            .config
726            .group_state_storage()
727            .state(group_id)
728            .await
729            .map_err(|e| MlsError::GroupStorageError(e.into_any_error()))?
730            .ok_or(MlsError::GroupNotFound)?;
731
732        let mut snapshot = Snapshot::mls_decode(&mut &*snapshot)?;
733        snapshot.state.public_tree.nodes = tree_data.0.into_owned();
734
735        Group::from_snapshot(self.config.clone(), snapshot).await
736    }
737
738    /// Request to join an existing [group](crate::group::Group).
739    ///
740    /// An existing group member will need to perform a
741    /// [commit](crate::Group::commit) to complete the add and the resulting
742    /// welcome message can be used by [join_group](Client::join_group).
743    #[cfg(feature = "by_ref_proposal")]
744    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
745    #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen_ignore)]
746    pub async fn external_add_proposal(
747        &self,
748        group_info: &MlsMessage,
749        tree_data: Option<crate::group::ExportedTree<'_>>,
750        authenticated_data: Vec<u8>,
751        key_package_extensions: ExtensionList,
752        leaf_node_extensions: ExtensionList,
753        timestamp: Option<MlsTime>,
754    ) -> Result<MlsMessage, MlsError> {
755        let protocol_version = group_info.version;
756
757        if !self.config.version_supported(protocol_version) && protocol_version == self.version {
758            return Err(MlsError::UnsupportedProtocolVersion(protocol_version));
759        }
760
761        let group_info = group_info
762            .as_group_info()
763            .ok_or(MlsError::UnexpectedMessageType)?;
764
765        let cipher_suite = group_info.group_context.cipher_suite;
766
767        let cipher_suite_provider = self
768            .config
769            .crypto_provider()
770            .cipher_suite_provider(cipher_suite)
771            .ok_or(MlsError::UnsupportedCipherSuite(cipher_suite))?;
772
773        crate::group::validate_tree_and_info_joiner(
774            protocol_version,
775            group_info,
776            tree_data,
777            &self.config.identity_provider(),
778            &cipher_suite_provider,
779            timestamp,
780        )
781        .await?;
782
783        let key_package = self
784            .generate_key_package(key_package_extensions, leaf_node_extensions, timestamp)
785            .await?
786            .key_package;
787
788        (key_package.cipher_suite == cipher_suite)
789            .then_some(())
790            .ok_or(MlsError::UnsupportedCipherSuite(cipher_suite))?;
791
792        let message = AuthenticatedContent::new_signed(
793            &cipher_suite_provider,
794            &group_info.group_context,
795            Sender::NewMemberProposal,
796            Content::Proposal(Box::new(Proposal::Add(Box::new(AddProposal {
797                key_package,
798            })))),
799            self.signer()?,
800            WireFormat::PublicMessage,
801            authenticated_data,
802        )
803        .await?;
804
805        let plaintext = PublicMessage {
806            content: message.content,
807            auth: message.auth,
808            membership_tag: None,
809        };
810
811        Ok(MlsMessage {
812            version: protocol_version,
813            payload: MlsMessagePayload::Plain(plaintext),
814        })
815    }
816
817    fn signer(&self) -> Result<&SignatureSecretKey, MlsError> {
818        self.signer.as_ref().ok_or(MlsError::SignerNotFound)
819    }
820
821    #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen_ignore)]
822    pub fn signing_identity(&self) -> Result<(&SigningIdentity, CipherSuite), MlsError> {
823        self.signing_identity
824            .as_ref()
825            .map(|(id, cs)| (id, *cs))
826            .ok_or(MlsError::SignerNotFound)
827    }
828
829    /// The [KeyPackageStorage] that this client was configured to use.
830    #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen_ignore)]
831    pub fn key_package_store(&self) -> <C as ClientConfig>::KeyPackageRepository {
832        self.config.key_package_repo()
833    }
834
835    /// The [PreSharedKeyStorage](crate::PreSharedKeyStorage) that
836    /// this client was configured to use.
837    #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen_ignore)]
838    pub fn secret_store(&self) -> <C as ClientConfig>::PskStore {
839        self.config.secret_store()
840    }
841
842    /// The [GroupStateStorage] that this client was configured to use.
843    #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen_ignore)]
844    pub fn group_state_storage(&self) -> <C as ClientConfig>::GroupStateStorage {
845        self.config.group_state_storage()
846    }
847
848    /// The [IdentityProvider](crate::IdentityProvider) that this client was configured to use.
849    #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen_ignore)]
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}