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