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