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