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