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