• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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         };
449 
450         let key_pkg_gen = key_package_generator
451             .generate(
452                 self.config.lifetime(),
453                 self.config.capabilities(),
454                 self.config.key_package_extensions(),
455                 self.config.leaf_node_extensions(),
456             )
457             .await?;
458 
459         let (id, key_package_data) = key_pkg_gen.to_storage()?;
460 
461         self.config
462             .key_package_repo()
463             .insert(id, key_package_data)
464             .await
465             .map_err(|e| MlsError::KeyPackageRepoError(e.into_any_error()))?;
466 
467         Ok(key_pkg_gen)
468     }
469 
470     /// Create a group with a specific group_id.
471     ///
472     /// This function behaves the same way as
473     /// [create_group](Client::create_group) except that it
474     /// specifies a specific unique group identifier to be used.
475     ///
476     /// # Warning
477     ///
478     /// It is recommended to use [create_group](Client::create_group)
479     /// instead of this function because it guarantees that group_id values
480     /// are globally unique.
481     #[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>482     pub async fn create_group_with_id(
483         &self,
484         group_id: Vec<u8>,
485         group_context_extensions: ExtensionList,
486     ) -> Result<Group<C>, MlsError> {
487         let (signing_identity, cipher_suite) = self.signing_identity()?;
488 
489         Group::new(
490             self.config.clone(),
491             Some(group_id),
492             cipher_suite,
493             self.version,
494             signing_identity.clone(),
495             group_context_extensions,
496             self.signer()?.clone(),
497         )
498         .await
499     }
500 
501     /// Create a MLS group.
502     ///
503     /// The `cipher_suite` provided must be supported by the
504     /// [CipherSuiteProvider](crate::CipherSuiteProvider)
505     /// that was used to build the client.
506     #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
create_group( &self, group_context_extensions: ExtensionList, ) -> Result<Group<C>, MlsError>507     pub async fn create_group(
508         &self,
509         group_context_extensions: ExtensionList,
510     ) -> Result<Group<C>, MlsError> {
511         let (signing_identity, cipher_suite) = self.signing_identity()?;
512 
513         Group::new(
514             self.config.clone(),
515             None,
516             cipher_suite,
517             self.version,
518             signing_identity.clone(),
519             group_context_extensions,
520             self.signer()?.clone(),
521         )
522         .await
523     }
524 
525     /// Join a MLS group via a welcome message created by a
526     /// [Commit](crate::group::CommitOutput).
527     ///
528     /// `tree_data` is required to be provided out of band if the client that
529     /// created `welcome_message` did not use the `ratchet_tree_extension`
530     /// according to [`MlsRules::commit_options`](`crate::MlsRules::commit_options`).
531     /// at the time the welcome message was created. `tree_data` can
532     /// be exported from a group using the
533     /// [export tree function](crate::group::Group::export_tree).
534     #[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>535     pub async fn join_group(
536         &self,
537         tree_data: Option<ExportedTree<'_>>,
538         welcome_message: &MlsMessage,
539     ) -> Result<(Group<C>, NewMemberInfo), MlsError> {
540         Group::join(
541             welcome_message,
542             tree_data,
543             self.config.clone(),
544             self.signer()?.clone(),
545         )
546         .await
547     }
548 
549     /// 0-RTT add to an existing [group](crate::group::Group)
550     ///
551     /// External commits allow for immediate entry into a
552     /// [group](crate::group::Group), even if all of the group members
553     /// are currently offline and unable to process messages. Sending an
554     /// external commit is only allowed for groups that have provided
555     /// a public `group_info_message` containing an
556     /// [ExternalPubExt](crate::extension::ExternalPubExt), which can be
557     /// generated by an existing group member using the
558     /// [group_info_message](crate::group::Group::group_info_message)
559     /// function.
560     ///
561     /// `tree_data` may be provided following the same rules as [Client::join_group]
562     ///
563     /// If PSKs are provided in `external_psks`, the
564     /// [PreSharedKeyStorage](crate::PreSharedKeyStorage)
565     /// used to configure the client will be searched to resolve their values.
566     ///
567     /// `to_remove` may be used to remove an existing member provided that the
568     /// identity of the existing group member at that [index](crate::group::Member::index)
569     /// is a [valid successor](crate::IdentityProvider::valid_successor)
570     /// of `signing_identity` as defined by the
571     /// [IdentityProvider](crate::IdentityProvider) that this client
572     /// was configured with.
573     ///
574     /// # Warning
575     ///
576     /// Only one external commit can be performed against a given group info.
577     /// There may also be security trade-offs to this approach.
578     ///
579     // TODO: Add a comment about forward secrecy and a pointer to the future
580     // book chapter on this topic
581     #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
commit_external( &self, group_info_msg: MlsMessage, ) -> Result<(Group<C>, MlsMessage), MlsError>582     pub async fn commit_external(
583         &self,
584         group_info_msg: MlsMessage,
585     ) -> Result<(Group<C>, MlsMessage), MlsError> {
586         ExternalCommitBuilder::new(
587             self.signer()?.clone(),
588             self.signing_identity()?.0.clone(),
589             self.config.clone(),
590         )
591         .build(group_info_msg)
592         .await
593     }
594 
external_commit_builder(&self) -> Result<ExternalCommitBuilder<C>, MlsError>595     pub fn external_commit_builder(&self) -> Result<ExternalCommitBuilder<C>, MlsError> {
596         Ok(ExternalCommitBuilder::new(
597             self.signer()?.clone(),
598             self.signing_identity()?.0.clone(),
599             self.config.clone(),
600         ))
601     }
602 
603     /// Load an existing group state into this client using the
604     /// [GroupStateStorage](crate::GroupStateStorage) that
605     /// this client was configured to use.
606     #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
607     #[inline(never)]
load_group(&self, group_id: &[u8]) -> Result<Group<C>, MlsError>608     pub async fn load_group(&self, group_id: &[u8]) -> Result<Group<C>, MlsError> {
609         let snapshot = self
610             .config
611             .group_state_storage()
612             .state(group_id)
613             .await
614             .map_err(|e| MlsError::GroupStorageError(e.into_any_error()))?
615             .ok_or(MlsError::GroupNotFound)?;
616 
617         let snapshot = Snapshot::mls_decode(&mut &*snapshot)?;
618 
619         Group::from_snapshot(self.config.clone(), snapshot).await
620     }
621 
622     /// Request to join an existing [group](crate::group::Group).
623     ///
624     /// An existing group member will need to perform a
625     /// [commit](crate::Group::commit) to complete the add and the resulting
626     /// welcome message can be used by [join_group](Client::join_group).
627     #[cfg(feature = "by_ref_proposal")]
628     #[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>629     pub async fn external_add_proposal(
630         &self,
631         group_info: &MlsMessage,
632         tree_data: Option<crate::group::ExportedTree<'_>>,
633         authenticated_data: Vec<u8>,
634     ) -> Result<MlsMessage, MlsError> {
635         let protocol_version = group_info.version;
636 
637         if !self.config.version_supported(protocol_version) && protocol_version == self.version {
638             return Err(MlsError::UnsupportedProtocolVersion(protocol_version));
639         }
640 
641         let group_info = group_info
642             .as_group_info()
643             .ok_or(MlsError::UnexpectedMessageType)?;
644 
645         let cipher_suite = group_info.group_context.cipher_suite;
646 
647         let cipher_suite_provider = self
648             .config
649             .crypto_provider()
650             .cipher_suite_provider(cipher_suite)
651             .ok_or(MlsError::UnsupportedCipherSuite(cipher_suite))?;
652 
653         crate::group::validate_group_info_joiner(
654             protocol_version,
655             group_info,
656             tree_data,
657             &self.config.identity_provider(),
658             &cipher_suite_provider,
659         )
660         .await?;
661 
662         let key_package = self.generate_key_package().await?.key_package;
663 
664         (key_package.cipher_suite == cipher_suite)
665             .then_some(())
666             .ok_or(MlsError::UnsupportedCipherSuite(cipher_suite))?;
667 
668         let message = AuthenticatedContent::new_signed(
669             &cipher_suite_provider,
670             &group_info.group_context,
671             Sender::NewMemberProposal,
672             Content::Proposal(Box::new(Proposal::Add(Box::new(AddProposal {
673                 key_package,
674             })))),
675             self.signer()?,
676             WireFormat::PublicMessage,
677             authenticated_data,
678         )
679         .await?;
680 
681         let plaintext = PublicMessage {
682             content: message.content,
683             auth: message.auth,
684             membership_tag: None,
685         };
686 
687         Ok(MlsMessage {
688             version: protocol_version,
689             payload: MlsMessagePayload::Plain(plaintext),
690         })
691     }
692 
signer(&self) -> Result<&SignatureSecretKey, MlsError>693     fn signer(&self) -> Result<&SignatureSecretKey, MlsError> {
694         self.signer.as_ref().ok_or(MlsError::SignerNotFound)
695     }
696 
697     #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen_ignore)]
signing_identity(&self) -> Result<(&SigningIdentity, CipherSuite), MlsError>698     pub fn signing_identity(&self) -> Result<(&SigningIdentity, CipherSuite), MlsError> {
699         self.signing_identity
700             .as_ref()
701             .map(|(id, cs)| (id, *cs))
702             .ok_or(MlsError::SignerNotFound)
703     }
704 
705     /// Returns key package extensions used by this client
key_package_extensions(&self) -> ExtensionList706     pub fn key_package_extensions(&self) -> ExtensionList {
707         self.config.key_package_extensions()
708     }
709 
710     /// The [KeyPackageStorage] that this client was configured to use.
711     #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen_ignore)]
key_package_store(&self) -> <C as ClientConfig>::KeyPackageRepository712     pub fn key_package_store(&self) -> <C as ClientConfig>::KeyPackageRepository {
713         self.config.key_package_repo()
714     }
715 
716     /// The [PreSharedKeyStorage](crate::PreSharedKeyStorage) that
717     /// this client was configured to use.
718     #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen_ignore)]
secret_store(&self) -> <C as ClientConfig>::PskStore719     pub fn secret_store(&self) -> <C as ClientConfig>::PskStore {
720         self.config.secret_store()
721     }
722 
723     /// The [GroupStateStorage] that this client was configured to use.
724     #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen_ignore)]
group_state_storage(&self) -> <C as ClientConfig>::GroupStateStorage725     pub fn group_state_storage(&self) -> <C as ClientConfig>::GroupStateStorage {
726         self.config.group_state_storage()
727     }
728 
729     /// The [IdentityProvider] that this client was configured to use.
730     #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen_ignore)]
identity_provider(&self) -> <C as ClientConfig>::IdentityProvider731     pub fn identity_provider(&self) -> <C as ClientConfig>::IdentityProvider {
732         self.config.identity_provider()
733     }
734 }
735 
736 #[cfg(test)]
737 pub(crate) mod test_utils {
738     use super::*;
739     use crate::identity::test_utils::get_test_signing_identity;
740 
741     pub use crate::client_builder::test_utils::{TestClientBuilder, TestClientConfig};
742 
743     pub const TEST_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::MLS_10;
744     pub const TEST_CIPHER_SUITE: CipherSuite = CipherSuite::P256_AES128;
745     pub const TEST_CUSTOM_PROPOSAL_TYPE: ProposalType = ProposalType::new(65001);
746 
747     #[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)748     pub async fn test_client_with_key_pkg(
749         protocol_version: ProtocolVersion,
750         cipher_suite: CipherSuite,
751         identity: &str,
752     ) -> (Client<TestClientConfig>, MlsMessage) {
753         test_client_with_key_pkg_custom(protocol_version, cipher_suite, identity, |_| {}).await
754     }
755 
756     #[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),757     pub async fn test_client_with_key_pkg_custom<F>(
758         protocol_version: ProtocolVersion,
759         cipher_suite: CipherSuite,
760         identity: &str,
761         mut config: F,
762     ) -> (Client<TestClientConfig>, MlsMessage)
763     where
764         F: FnMut(&mut TestClientConfig),
765     {
766         let (identity, secret_key) =
767             get_test_signing_identity(cipher_suite, identity.as_bytes()).await;
768 
769         let mut client = TestClientBuilder::new_for_test()
770             .used_protocol_version(protocol_version)
771             .signing_identity(identity.clone(), secret_key, cipher_suite)
772             .build();
773 
774         config(&mut client.config);
775 
776         let key_package = client.generate_key_package_message().await.unwrap();
777 
778         (client, key_package)
779     }
780 }
781 
782 #[cfg(test)]
783 mod tests {
784     use super::test_utils::*;
785 
786     use super::*;
787     use crate::{
788         crypto::test_utils::TestCryptoProvider,
789         identity::test_utils::{get_test_basic_credential, get_test_signing_identity},
790         tree_kem::leaf_node::LeafNodeSource,
791     };
792     use assert_matches::assert_matches;
793 
794     use crate::{
795         group::{
796             message_processor::ProposalMessageDescription,
797             proposal::Proposal,
798             test_utils::{test_group, test_group_custom_config},
799             ReceivedMessage,
800         },
801         psk::{ExternalPskId, PreSharedKey},
802     };
803 
804     use alloc::vec;
805 
806     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
test_keygen()807     async fn test_keygen() {
808         // This is meant to test the inputs to the internal key package generator
809         // See KeyPackageGenerator tests for key generation specific tests
810         for (protocol_version, cipher_suite) in ProtocolVersion::all().flat_map(|p| {
811             TestCryptoProvider::all_supported_cipher_suites()
812                 .into_iter()
813                 .map(move |cs| (p, cs))
814         }) {
815             let (identity, secret_key) = get_test_signing_identity(cipher_suite, b"foo").await;
816 
817             let client = TestClientBuilder::new_for_test()
818                 .signing_identity(identity.clone(), secret_key, cipher_suite)
819                 .build();
820 
821             // TODO: Tests around extensions
822             let key_package = client.generate_key_package_message().await.unwrap();
823 
824             assert_eq!(key_package.version, protocol_version);
825 
826             let key_package = key_package.into_key_package().unwrap();
827 
828             assert_eq!(key_package.cipher_suite, cipher_suite);
829 
830             assert_eq!(
831                 &key_package.leaf_node.signing_identity.credential,
832                 &get_test_basic_credential(b"foo".to_vec())
833             );
834 
835             assert_eq!(key_package.leaf_node.signing_identity, identity);
836 
837             let capabilities = key_package.leaf_node.ungreased_capabilities();
838             assert_eq!(capabilities, client.config.capabilities());
839 
840             let client_lifetime = client.config.lifetime();
841             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));
842         }
843     }
844 
845     #[cfg(feature = "by_ref_proposal")]
846     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
new_member_add_proposal_adds_to_group()847     async fn new_member_add_proposal_adds_to_group() {
848         let mut alice_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
849 
850         let (bob_identity, secret_key) = get_test_signing_identity(TEST_CIPHER_SUITE, b"bob").await;
851 
852         let bob = TestClientBuilder::new_for_test()
853             .signing_identity(bob_identity.clone(), secret_key, TEST_CIPHER_SUITE)
854             .build();
855 
856         let proposal = bob
857             .external_add_proposal(
858                 &alice_group.group.group_info_message(true).await.unwrap(),
859                 None,
860                 vec![],
861             )
862             .await
863             .unwrap();
864 
865         let message = alice_group
866             .group
867             .process_incoming_message(proposal)
868             .await
869             .unwrap();
870 
871         assert_matches!(
872             message,
873             ReceivedMessage::Proposal(ProposalMessageDescription {
874                 proposal: Proposal::Add(p), ..}
875             ) if p.key_package.leaf_node.signing_identity == bob_identity
876         );
877 
878         alice_group.group.commit(vec![]).await.unwrap();
879         alice_group.group.apply_pending_commit().await.unwrap();
880 
881         // Check that the new member is in the group
882         assert!(alice_group
883             .group
884             .roster()
885             .members_iter()
886             .any(|member| member.signing_identity == bob_identity))
887     }
888 
889     #[cfg(feature = "psk")]
890     #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
join_via_external_commit(do_remove: bool, with_psk: bool) -> Result<(), MlsError>891     async fn join_via_external_commit(do_remove: bool, with_psk: bool) -> Result<(), MlsError> {
892         // An external commit cannot be the first commit in a group as it requires
893         // interim_transcript_hash to be computed from the confirmed_transcript_hash and
894         // confirmation_tag, which is not the case for the initial interim_transcript_hash.
895 
896         let psk = PreSharedKey::from(b"psk".to_vec());
897         let psk_id = ExternalPskId::new(b"psk id".to_vec());
898 
899         let mut alice_group =
900             test_group_custom_config(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, |c| {
901                 c.psk(psk_id.clone(), psk.clone())
902             })
903             .await;
904 
905         let (mut bob_group, _) = alice_group
906             .join_with_custom_config("bob", false, |c| {
907                 c.0.psk_store.insert(psk_id.clone(), psk.clone());
908             })
909             .await
910             .unwrap();
911 
912         let group_info_msg = alice_group
913             .group
914             .group_info_message_allowing_ext_commit(true)
915             .await
916             .unwrap();
917 
918         let new_client_id = if do_remove { "bob" } else { "charlie" };
919 
920         let (new_client_identity, secret_key) =
921             get_test_signing_identity(TEST_CIPHER_SUITE, new_client_id.as_bytes()).await;
922 
923         let new_client = TestClientBuilder::new_for_test()
924             .psk(psk_id.clone(), psk)
925             .signing_identity(new_client_identity.clone(), secret_key, TEST_CIPHER_SUITE)
926             .build();
927 
928         let mut builder = new_client.external_commit_builder().unwrap();
929 
930         if do_remove {
931             builder = builder.with_removal(1);
932         }
933 
934         if with_psk {
935             builder = builder.with_external_psk(psk_id);
936         }
937 
938         let (new_group, external_commit) = builder.build(group_info_msg).await?;
939 
940         let num_members = if do_remove { 2 } else { 3 };
941 
942         assert_eq!(new_group.roster().members_iter().count(), num_members);
943 
944         let _ = alice_group
945             .group
946             .process_incoming_message(external_commit.clone())
947             .await
948             .unwrap();
949 
950         let bob_current_epoch = bob_group.group.current_epoch();
951 
952         let message = bob_group
953             .group
954             .process_incoming_message(external_commit)
955             .await
956             .unwrap();
957 
958         assert!(alice_group.group.roster().members_iter().count() == num_members);
959 
960         if !do_remove {
961             assert!(bob_group.group.roster().members_iter().count() == num_members);
962         } else {
963             // Bob was removed so his epoch must stay the same
964             assert_eq!(bob_group.group.current_epoch(), bob_current_epoch);
965 
966             #[cfg(feature = "state_update")]
967             assert_matches!(message, ReceivedMessage::Commit(desc) if !desc.state_update.active);
968 
969             #[cfg(not(feature = "state_update"))]
970             assert_matches!(message, ReceivedMessage::Commit(_));
971         }
972 
973         // Comparing epoch authenticators is sufficient to check that members are in sync.
974         assert_eq!(
975             alice_group.group.epoch_authenticator().unwrap(),
976             new_group.epoch_authenticator().unwrap()
977         );
978 
979         Ok(())
980     }
981 
982     #[cfg(feature = "psk")]
983     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
test_external_commit()984     async fn test_external_commit() {
985         // New member can join
986         join_via_external_commit(false, false).await.unwrap();
987         // New member can remove an old copy of themselves
988         join_via_external_commit(true, false).await.unwrap();
989         // New member can inject a PSK
990         join_via_external_commit(false, true).await.unwrap();
991         // All works together
992         join_via_external_commit(true, true).await.unwrap();
993     }
994 
995     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
creating_an_external_commit_requires_a_group_info_message()996     async fn creating_an_external_commit_requires_a_group_info_message() {
997         let (alice_identity, secret_key) =
998             get_test_signing_identity(TEST_CIPHER_SUITE, b"alice").await;
999 
1000         let alice = TestClientBuilder::new_for_test()
1001             .signing_identity(alice_identity.clone(), secret_key, TEST_CIPHER_SUITE)
1002             .build();
1003 
1004         let msg = alice.generate_key_package_message().await.unwrap();
1005         let res = alice.commit_external(msg).await.map(|_| ());
1006 
1007         assert_matches!(res, Err(MlsError::UnexpectedMessageType));
1008     }
1009 
1010     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
external_commit_with_invalid_group_info_fails()1011     async fn external_commit_with_invalid_group_info_fails() {
1012         let mut alice_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1013         let mut bob_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await;
1014 
1015         bob_group.group.commit(vec![]).await.unwrap();
1016         bob_group.group.apply_pending_commit().await.unwrap();
1017 
1018         let group_info_msg = bob_group
1019             .group
1020             .group_info_message_allowing_ext_commit(true)
1021             .await
1022             .unwrap();
1023 
1024         let (carol_identity, secret_key) =
1025             get_test_signing_identity(TEST_CIPHER_SUITE, b"carol").await;
1026 
1027         let carol = TestClientBuilder::new_for_test()
1028             .signing_identity(carol_identity, secret_key, TEST_CIPHER_SUITE)
1029             .build();
1030 
1031         let (_, external_commit) = carol
1032             .external_commit_builder()
1033             .unwrap()
1034             .build(group_info_msg)
1035             .await
1036             .unwrap();
1037 
1038         // If Carol tries to join Alice's group using the group info from Bob's group, that fails.
1039         let res = alice_group
1040             .group
1041             .process_incoming_message(external_commit)
1042             .await;
1043         assert_matches!(res, Err(_));
1044     }
1045 
1046     #[test]
builder_can_be_obtained_from_client_to_edit_properties_for_new_client()1047     fn builder_can_be_obtained_from_client_to_edit_properties_for_new_client() {
1048         let alice = TestClientBuilder::new_for_test()
1049             .extension_type(33.into())
1050             .build();
1051         let bob = alice.to_builder().extension_type(34.into()).build();
1052         assert_eq!(bob.config.supported_extensions(), [33, 34].map(Into::into));
1053     }
1054 }
1055