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