// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // Copyright by contributors to this project. // SPDX-License-Identifier: (Apache-2.0 OR MIT) use crate::cipher_suite::CipherSuite; use crate::client_builder::{recreate_config, BaseConfig, ClientBuilder, MakeConfig}; use crate::client_config::ClientConfig; use crate::group::framing::MlsMessage; #[cfg(feature = "by_ref_proposal")] use crate::group::{ framing::{Content, MlsMessagePayload, PublicMessage, Sender, WireFormat}, message_signature::AuthenticatedContent, proposal::{AddProposal, Proposal}, }; use crate::group::{snapshot::Snapshot, ExportedTree, Group, NewMemberInfo}; use crate::identity::SigningIdentity; use crate::key_package::{KeyPackageGeneration, KeyPackageGenerator}; use crate::protocol_version::ProtocolVersion; use crate::tree_kem::node::NodeIndex; use alloc::vec::Vec; use mls_rs_codec::MlsDecode; use mls_rs_core::crypto::{CryptoProvider, SignatureSecretKey}; use mls_rs_core::error::{AnyError, IntoAnyError}; use mls_rs_core::extension::{ExtensionError, ExtensionList, ExtensionType}; use mls_rs_core::group::{GroupStateStorage, ProposalType}; use mls_rs_core::identity::CredentialType; use mls_rs_core::key_package::KeyPackageStorage; use crate::group::external_commit::ExternalCommitBuilder; #[cfg(feature = "by_ref_proposal")] use alloc::boxed::Box; #[derive(Debug)] #[cfg_attr(feature = "std", derive(thiserror::Error))] #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::enum_to_error_code)] #[non_exhaustive] pub enum MlsError { #[cfg_attr(feature = "std", error(transparent))] IdentityProviderError(AnyError), #[cfg_attr(feature = "std", error(transparent))] CryptoProviderError(AnyError), #[cfg_attr(feature = "std", error(transparent))] KeyPackageRepoError(AnyError), #[cfg_attr(feature = "std", error(transparent))] GroupStorageError(AnyError), #[cfg_attr(feature = "std", error(transparent))] PskStoreError(AnyError), #[cfg_attr(feature = "std", error(transparent))] MlsRulesError(AnyError), #[cfg_attr(feature = "std", error(transparent))] SerializationError(AnyError), #[cfg_attr(feature = "std", error(transparent))] ExtensionError(AnyError), #[cfg_attr(feature = "std", error("Cipher suite does not match"))] CipherSuiteMismatch, #[cfg_attr(feature = "std", error("Invalid commit, missing required path"))] CommitMissingPath, #[cfg_attr(feature = "std", error("plaintext message for incorrect epoch"))] InvalidEpoch, #[cfg_attr(feature = "std", error("invalid signature found"))] InvalidSignature, #[cfg_attr(feature = "std", error("invalid confirmation tag"))] InvalidConfirmationTag, #[cfg_attr(feature = "std", error("invalid membership tag"))] InvalidMembershipTag, #[cfg_attr(feature = "std", error("corrupt private key, missing required values"))] InvalidTreeKemPrivateKey, #[cfg_attr(feature = "std", error("key package not found, unable to process"))] WelcomeKeyPackageNotFound, #[cfg_attr(feature = "std", error("leaf not found in tree for index {0}"))] LeafNotFound(u32), #[cfg_attr(feature = "std", error("message from self can't be processed"))] CantProcessMessageFromSelf, #[cfg_attr( feature = "std", error("pending proposals found, commit required before application messages can be sent") )] CommitRequired, #[cfg_attr( feature = "std", error("ratchet tree not provided or discovered in GroupInfo") )] RatchetTreeNotFound, #[cfg_attr(feature = "std", error("External sender cannot commit"))] ExternalSenderCannotCommit, #[cfg_attr(feature = "std", error("Unsupported protocol version {0:?}"))] UnsupportedProtocolVersion(ProtocolVersion), #[cfg_attr(feature = "std", error("Protocol version mismatch"))] ProtocolVersionMismatch, #[cfg_attr(feature = "std", error("Unsupported cipher suite {0:?}"))] UnsupportedCipherSuite(CipherSuite), #[cfg_attr(feature = "std", error("Signing key of external sender is unknown"))] UnknownSigningIdentityForExternalSender, #[cfg_attr( feature = "std", error("External proposals are disabled for this group") )] ExternalProposalsDisabled, #[cfg_attr( feature = "std", error("Signing identity is not allowed to externally propose") )] InvalidExternalSigningIdentity, #[cfg_attr(feature = "std", error("Missing ExternalPub extension"))] MissingExternalPubExtension, #[cfg_attr(feature = "std", error("Epoch not found"))] EpochNotFound, #[cfg_attr(feature = "std", error("Unencrypted application message"))] UnencryptedApplicationMessage, #[cfg_attr( feature = "std", error("NewMemberCommit sender type can only be used to send Commit content") )] ExpectedCommitForNewMemberCommit, #[cfg_attr( feature = "std", error("NewMemberProposal sender type can only be used to send add proposals") )] ExpectedAddProposalForNewMemberProposal, #[cfg_attr( feature = "std", error("External commit missing ExternalInit proposal") )] ExternalCommitMissingExternalInit, #[cfg_attr( feature = "std", error( "A ReIinit has been applied. The next action must be creating or receiving a welcome." ) )] GroupUsedAfterReInit, #[cfg_attr(feature = "std", error("Pending ReIinit not found."))] PendingReInitNotFound, #[cfg_attr( feature = "std", error("The extensions in the welcome message and in the reinit do not match.") )] ReInitExtensionsMismatch, #[cfg_attr(feature = "std", error("signer not found for given identity"))] SignerNotFound, #[cfg_attr(feature = "std", error("commit already pending"))] ExistingPendingCommit, #[cfg_attr(feature = "std", error("pending commit not found"))] PendingCommitNotFound, #[cfg_attr(feature = "std", error("unexpected message type for action"))] UnexpectedMessageType, #[cfg_attr( feature = "std", error("membership tag on MlsPlaintext for non-member sender") )] MembershipTagForNonMember, #[cfg_attr(feature = "std", error("No member found for given identity id."))] MemberNotFound, #[cfg_attr(feature = "std", error("group not found"))] GroupNotFound, #[cfg_attr(feature = "std", error("unexpected PSK ID"))] UnexpectedPskId, #[cfg_attr(feature = "std", error("invalid sender for content type"))] InvalidSender, #[cfg_attr(feature = "std", error("GroupID mismatch"))] GroupIdMismatch, #[cfg_attr(feature = "std", error("storage retention can not be zero"))] NonZeroRetentionRequired, #[cfg_attr(feature = "std", error("Too many PSK IDs to compute PSK secret"))] TooManyPskIds, #[cfg_attr(feature = "std", error("Missing required Psk"))] MissingRequiredPsk, #[cfg_attr(feature = "std", error("Old group state not found"))] OldGroupStateNotFound, #[cfg_attr(feature = "std", error("leaf secret already consumed"))] InvalidLeafConsumption, #[cfg_attr(feature = "std", error("key not available, invalid generation {0}"))] KeyMissing(u32), #[cfg_attr( feature = "std", error("requested generation {0} is too far ahead of current generation") )] InvalidFutureGeneration(u32), #[cfg_attr(feature = "std", error("leaf node has no children"))] LeafNodeNoChildren, #[cfg_attr(feature = "std", error("root node has no parent"))] LeafNodeNoParent, #[cfg_attr(feature = "std", error("index out of range"))] InvalidTreeIndex, #[cfg_attr(feature = "std", error("time overflow"))] TimeOverflow, #[cfg_attr(feature = "std", error("invalid leaf_node_source"))] InvalidLeafNodeSource, #[cfg_attr(feature = "std", error("key package has expired or is not valid yet"))] InvalidLifetime, #[cfg_attr(feature = "std", error("required extension not found"))] RequiredExtensionNotFound(ExtensionType), #[cfg_attr(feature = "std", error("required proposal not found"))] RequiredProposalNotFound(ProposalType), #[cfg_attr(feature = "std", error("required credential not found"))] RequiredCredentialNotFound(CredentialType), #[cfg_attr(feature = "std", error("capabilities must describe extensions used"))] ExtensionNotInCapabilities(ExtensionType), #[cfg_attr(feature = "std", error("expected non-blank node"))] ExpectedNode, #[cfg_attr(feature = "std", error("node index is out of bounds {0}"))] InvalidNodeIndex(NodeIndex), #[cfg_attr(feature = "std", error("unexpected empty node found"))] UnexpectedEmptyNode, #[cfg_attr( feature = "std", error("duplicate signature key, hpke key or identity found at index {0}") )] DuplicateLeafData(u32), #[cfg_attr( feature = "std", error("In-use credential type not supported by new leaf at index") )] InUseCredentialTypeUnsupportedByNewLeaf, #[cfg_attr( feature = "std", error("Not all members support the credential type used by new leaf") )] CredentialTypeOfNewLeafIsUnsupported, #[cfg_attr( feature = "std", error("the length of the update path is different than the length of the direct path") )] WrongPathLen, #[cfg_attr( feature = "std", error("same HPKE leaf key before and after applying the update path for leaf {0}") )] SameHpkeKey(u32), #[cfg_attr(feature = "std", error("init key is not valid for cipher suite"))] InvalidInitKey, #[cfg_attr( feature = "std", error("init key can not be equal to leaf node public key") )] InitLeafKeyEquality, #[cfg_attr(feature = "std", error("different identity in update for leaf {0}"))] DifferentIdentityInUpdate(u32), #[cfg_attr(feature = "std", error("update path pub key mismatch"))] PubKeyMismatch, #[cfg_attr(feature = "std", error("tree hash mismatch"))] TreeHashMismatch, #[cfg_attr(feature = "std", error("bad update: no suitable secret key"))] UpdateErrorNoSecretKey, #[cfg_attr(feature = "std", error("invalid lca, not found on direct path"))] LcaNotFoundInDirectPath, #[cfg_attr(feature = "std", error("update path parent hash mismatch"))] ParentHashMismatch, #[cfg_attr(feature = "std", error("unexpected pattern of unmerged leaves"))] UnmergedLeavesMismatch, #[cfg_attr(feature = "std", error("empty tree"))] UnexpectedEmptyTree, #[cfg_attr(feature = "std", error("trailing blanks"))] UnexpectedTrailingBlanks, // Proposal Rules errors #[cfg_attr( feature = "std", error("Commiter must not include any update proposals generated by the commiter") )] InvalidCommitSelfUpdate, #[cfg_attr(feature = "std", error("A PreSharedKey proposal must have a PSK of type External or type Resumption and usage Application"))] InvalidTypeOrUsageInPreSharedKeyProposal, #[cfg_attr(feature = "std", error("psk nonce length does not match cipher suite"))] InvalidPskNonceLength, #[cfg_attr( feature = "std", error("ReInit proposal protocol version is less than the version of the original group") )] InvalidProtocolVersionInReInit, #[cfg_attr(feature = "std", error("More than one proposal applying to leaf: {0}"))] MoreThanOneProposalForLeaf(u32), #[cfg_attr( feature = "std", error("More than one GroupContextExtensions proposal") )] MoreThanOneGroupContextExtensionsProposal, #[cfg_attr(feature = "std", error("Invalid proposal type for sender"))] InvalidProposalTypeForSender, #[cfg_attr( feature = "std", error("External commit must have exactly one ExternalInit proposal") )] ExternalCommitMustHaveExactlyOneExternalInit, #[cfg_attr(feature = "std", error("External commit must have a new leaf"))] ExternalCommitMustHaveNewLeaf, #[cfg_attr( feature = "std", error("External commit contains removal of other identity") )] ExternalCommitRemovesOtherIdentity, #[cfg_attr( feature = "std", error("External commit contains more than one Remove proposal") )] ExternalCommitWithMoreThanOneRemove, #[cfg_attr(feature = "std", error("Duplicate PSK IDs"))] DuplicatePskIds, #[cfg_attr( feature = "std", error("Invalid proposal type {0:?} in external commit") )] InvalidProposalTypeInExternalCommit(ProposalType), #[cfg_attr(feature = "std", error("Committer can not remove themselves"))] CommitterSelfRemoval, #[cfg_attr( feature = "std", error("Only members can commit proposals by reference") )] OnlyMembersCanCommitProposalsByRef, #[cfg_attr(feature = "std", error("Other proposal with ReInit"))] OtherProposalWithReInit, #[cfg_attr(feature = "std", error("Unsupported group extension {0:?}"))] UnsupportedGroupExtension(ExtensionType), #[cfg_attr(feature = "std", error("Unsupported custom proposal type {0:?}"))] UnsupportedCustomProposal(ProposalType), #[cfg_attr(feature = "std", error("by-ref proposal not found"))] ProposalNotFound, #[cfg_attr( feature = "std", error("Removing non-existing member (or removing a member twice)") )] RemovingNonExistingMember, #[cfg_attr(feature = "std", error("Updated identity not a valid successor"))] InvalidSuccessor, #[cfg_attr( feature = "std", error("Updating non-existing member (or updating a member twice)") )] UpdatingNonExistingMember, #[cfg_attr(feature = "std", error("Failed generating next path secret"))] FailedGeneratingPathSecret, #[cfg_attr(feature = "std", error("Invalid group info"))] InvalidGroupInfo, #[cfg_attr(feature = "std", error("Invalid welcome message"))] InvalidWelcomeMessage, } impl IntoAnyError for MlsError { #[cfg(feature = "std")] fn into_dyn_error(self) -> Result<Box<dyn std::error::Error + Send + Sync>, Self> { Ok(self.into()) } } impl From<mls_rs_codec::Error> for MlsError { #[inline] fn from(e: mls_rs_codec::Error) -> Self { MlsError::SerializationError(e.into_any_error()) } } impl From<ExtensionError> for MlsError { #[inline] fn from(e: ExtensionError) -> Self { MlsError::ExtensionError(e.into_any_error()) } } /// MLS client used to create key packages and manage groups. /// /// [`Client::builder`] can be used to instantiate it. /// /// Clients are able to support multiple protocol versions, ciphersuites /// and underlying identities used to join groups and generate key packages. /// Applications may decide to create one or many clients depending on their /// specific needs. #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::ffi_type(opaque))] #[derive(Clone, Debug)] pub struct Client<C> { pub(crate) config: C, pub(crate) signing_identity: Option<(SigningIdentity, CipherSuite)>, pub(crate) signer: Option<SignatureSecretKey>, pub(crate) version: ProtocolVersion, } impl Client<()> { /// Returns a [`ClientBuilder`] /// used to configure client preferences and providers. pub fn builder() -> ClientBuilder<BaseConfig> { ClientBuilder::new() } } #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen)] impl<C> Client<C> where C: ClientConfig + Clone, { pub(crate) fn new( config: C, signer: Option<SignatureSecretKey>, signing_identity: Option<(SigningIdentity, CipherSuite)>, version: ProtocolVersion, ) -> Self { Client { config, signer, signing_identity, version, } } #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen_ignore)] pub fn to_builder(&self) -> ClientBuilder<MakeConfig<C>> { ClientBuilder::from_config(recreate_config( self.config.clone(), self.signer.clone(), self.signing_identity.clone(), self.version, )) } /// Creates a new key package message that can be used to to add this /// client to a [Group](crate::group::Group). Each call to this function /// will produce a unique value that is signed by `signing_identity`. /// /// The secret keys for the resulting key package message will be stored in /// the [KeyPackageStorage](crate::KeyPackageStorage) /// that was used to configure the client and will /// automatically be erased when this key package is used to /// [join a group](Client::join_group). /// /// # Warning /// /// A key package message may only be used once. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub async fn generate_key_package_message(&self) -> Result<MlsMessage, MlsError> { Ok(self.generate_key_package().await?.key_package_message()) } #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] async fn generate_key_package(&self) -> Result<KeyPackageGeneration, MlsError> { let (signing_identity, cipher_suite) = self.signing_identity()?; let cipher_suite_provider = self .config .crypto_provider() .cipher_suite_provider(cipher_suite) .ok_or(MlsError::UnsupportedCipherSuite(cipher_suite))?; let key_package_generator = KeyPackageGenerator { protocol_version: self.version, cipher_suite_provider: &cipher_suite_provider, signing_key: self.signer()?, signing_identity, identity_provider: &self.config.identity_provider(), }; let key_pkg_gen = key_package_generator .generate( self.config.lifetime(), self.config.capabilities(), self.config.key_package_extensions(), self.config.leaf_node_extensions(), ) .await?; let (id, key_package_data) = key_pkg_gen.to_storage()?; self.config .key_package_repo() .insert(id, key_package_data) .await .map_err(|e| MlsError::KeyPackageRepoError(e.into_any_error()))?; Ok(key_pkg_gen) } /// Create a group with a specific group_id. /// /// This function behaves the same way as /// [create_group](Client::create_group) except that it /// specifies a specific unique group identifier to be used. /// /// # Warning /// /// It is recommended to use [create_group](Client::create_group) /// instead of this function because it guarantees that group_id values /// are globally unique. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub async fn create_group_with_id( &self, group_id: Vec<u8>, group_context_extensions: ExtensionList, ) -> Result<Group<C>, MlsError> { let (signing_identity, cipher_suite) = self.signing_identity()?; Group::new( self.config.clone(), Some(group_id), cipher_suite, self.version, signing_identity.clone(), group_context_extensions, self.signer()?.clone(), ) .await } /// Create a MLS group. /// /// The `cipher_suite` provided must be supported by the /// [CipherSuiteProvider](crate::CipherSuiteProvider) /// that was used to build the client. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub async fn create_group( &self, group_context_extensions: ExtensionList, ) -> Result<Group<C>, MlsError> { let (signing_identity, cipher_suite) = self.signing_identity()?; Group::new( self.config.clone(), None, cipher_suite, self.version, signing_identity.clone(), group_context_extensions, self.signer()?.clone(), ) .await } /// Join a MLS group via a welcome message created by a /// [Commit](crate::group::CommitOutput). /// /// `tree_data` is required to be provided out of band if the client that /// created `welcome_message` did not use the `ratchet_tree_extension` /// according to [`MlsRules::commit_options`](`crate::MlsRules::commit_options`). /// at the time the welcome message was created. `tree_data` can /// be exported from a group using the /// [export tree function](crate::group::Group::export_tree). #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub async fn join_group( &self, tree_data: Option<ExportedTree<'_>>, welcome_message: &MlsMessage, ) -> Result<(Group<C>, NewMemberInfo), MlsError> { Group::join( welcome_message, tree_data, self.config.clone(), self.signer()?.clone(), ) .await } /// 0-RTT add to an existing [group](crate::group::Group) /// /// External commits allow for immediate entry into a /// [group](crate::group::Group), even if all of the group members /// are currently offline and unable to process messages. Sending an /// external commit is only allowed for groups that have provided /// a public `group_info_message` containing an /// [ExternalPubExt](crate::extension::ExternalPubExt), which can be /// generated by an existing group member using the /// [group_info_message](crate::group::Group::group_info_message) /// function. /// /// `tree_data` may be provided following the same rules as [Client::join_group] /// /// If PSKs are provided in `external_psks`, the /// [PreSharedKeyStorage](crate::PreSharedKeyStorage) /// used to configure the client will be searched to resolve their values. /// /// `to_remove` may be used to remove an existing member provided that the /// identity of the existing group member at that [index](crate::group::Member::index) /// is a [valid successor](crate::IdentityProvider::valid_successor) /// of `signing_identity` as defined by the /// [IdentityProvider](crate::IdentityProvider) that this client /// was configured with. /// /// # Warning /// /// Only one external commit can be performed against a given group info. /// There may also be security trade-offs to this approach. /// // TODO: Add a comment about forward secrecy and a pointer to the future // book chapter on this topic #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub async fn commit_external( &self, group_info_msg: MlsMessage, ) -> Result<(Group<C>, MlsMessage), MlsError> { ExternalCommitBuilder::new( self.signer()?.clone(), self.signing_identity()?.0.clone(), self.config.clone(), ) .build(group_info_msg) .await } pub fn external_commit_builder(&self) -> Result<ExternalCommitBuilder<C>, MlsError> { Ok(ExternalCommitBuilder::new( self.signer()?.clone(), self.signing_identity()?.0.clone(), self.config.clone(), )) } /// Load an existing group state into this client using the /// [GroupStateStorage](crate::GroupStateStorage) that /// this client was configured to use. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] #[inline(never)] pub async fn load_group(&self, group_id: &[u8]) -> Result<Group<C>, MlsError> { let snapshot = self .config .group_state_storage() .state(group_id) .await .map_err(|e| MlsError::GroupStorageError(e.into_any_error()))? .ok_or(MlsError::GroupNotFound)?; let snapshot = Snapshot::mls_decode(&mut &*snapshot)?; Group::from_snapshot(self.config.clone(), snapshot).await } /// Request to join an existing [group](crate::group::Group). /// /// An existing group member will need to perform a /// [commit](crate::Group::commit) to complete the add and the resulting /// welcome message can be used by [join_group](Client::join_group). #[cfg(feature = "by_ref_proposal")] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub async fn external_add_proposal( &self, group_info: &MlsMessage, tree_data: Option<crate::group::ExportedTree<'_>>, authenticated_data: Vec<u8>, ) -> Result<MlsMessage, MlsError> { let protocol_version = group_info.version; if !self.config.version_supported(protocol_version) && protocol_version == self.version { return Err(MlsError::UnsupportedProtocolVersion(protocol_version)); } let group_info = group_info .as_group_info() .ok_or(MlsError::UnexpectedMessageType)?; let cipher_suite = group_info.group_context.cipher_suite; let cipher_suite_provider = self .config .crypto_provider() .cipher_suite_provider(cipher_suite) .ok_or(MlsError::UnsupportedCipherSuite(cipher_suite))?; crate::group::validate_group_info_joiner( protocol_version, group_info, tree_data, &self.config.identity_provider(), &cipher_suite_provider, ) .await?; let key_package = self.generate_key_package().await?.key_package; (key_package.cipher_suite == cipher_suite) .then_some(()) .ok_or(MlsError::UnsupportedCipherSuite(cipher_suite))?; let message = AuthenticatedContent::new_signed( &cipher_suite_provider, &group_info.group_context, Sender::NewMemberProposal, Content::Proposal(Box::new(Proposal::Add(Box::new(AddProposal { key_package, })))), self.signer()?, WireFormat::PublicMessage, authenticated_data, ) .await?; let plaintext = PublicMessage { content: message.content, auth: message.auth, membership_tag: None, }; Ok(MlsMessage { version: protocol_version, payload: MlsMessagePayload::Plain(plaintext), }) } fn signer(&self) -> Result<&SignatureSecretKey, MlsError> { self.signer.as_ref().ok_or(MlsError::SignerNotFound) } #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen_ignore)] pub fn signing_identity(&self) -> Result<(&SigningIdentity, CipherSuite), MlsError> { self.signing_identity .as_ref() .map(|(id, cs)| (id, *cs)) .ok_or(MlsError::SignerNotFound) } /// Returns key package extensions used by this client pub fn key_package_extensions(&self) -> ExtensionList { self.config.key_package_extensions() } /// The [KeyPackageStorage] that this client was configured to use. #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen_ignore)] pub fn key_package_store(&self) -> <C as ClientConfig>::KeyPackageRepository { self.config.key_package_repo() } /// The [PreSharedKeyStorage](crate::PreSharedKeyStorage) that /// this client was configured to use. #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen_ignore)] pub fn secret_store(&self) -> <C as ClientConfig>::PskStore { self.config.secret_store() } /// The [GroupStateStorage] that this client was configured to use. #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen_ignore)] pub fn group_state_storage(&self) -> <C as ClientConfig>::GroupStateStorage { self.config.group_state_storage() } } #[cfg(test)] pub(crate) mod test_utils { use super::*; use crate::identity::test_utils::get_test_signing_identity; pub use crate::client_builder::test_utils::{TestClientBuilder, TestClientConfig}; pub const TEST_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::MLS_10; pub const TEST_CIPHER_SUITE: CipherSuite = CipherSuite::P256_AES128; pub const TEST_CUSTOM_PROPOSAL_TYPE: ProposalType = ProposalType::new(65001); #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub async fn test_client_with_key_pkg( protocol_version: ProtocolVersion, cipher_suite: CipherSuite, identity: &str, ) -> (Client<TestClientConfig>, MlsMessage) { test_client_with_key_pkg_custom(protocol_version, cipher_suite, identity, |_| {}).await } #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub async fn 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), { let (identity, secret_key) = get_test_signing_identity(cipher_suite, identity.as_bytes()).await; let mut client = TestClientBuilder::new_for_test() .used_protocol_version(protocol_version) .signing_identity(identity.clone(), secret_key, cipher_suite) .build(); config(&mut client.config); let key_package = client.generate_key_package_message().await.unwrap(); (client, key_package) } } #[cfg(test)] mod tests { use super::test_utils::*; use super::*; use crate::{ crypto::test_utils::TestCryptoProvider, identity::test_utils::{get_test_basic_credential, get_test_signing_identity}, tree_kem::leaf_node::LeafNodeSource, }; use assert_matches::assert_matches; use crate::{ group::{ message_processor::ProposalMessageDescription, proposal::Proposal, test_utils::{test_group, test_group_custom_config}, ReceivedMessage, }, psk::{ExternalPskId, PreSharedKey}, }; use alloc::vec; #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn test_keygen() { // This is meant to test the inputs to the internal key package generator // See KeyPackageGenerator tests for key generation specific tests for (protocol_version, cipher_suite) in ProtocolVersion::all().flat_map(|p| { TestCryptoProvider::all_supported_cipher_suites() .into_iter() .map(move |cs| (p, cs)) }) { let (identity, secret_key) = get_test_signing_identity(cipher_suite, b"foo").await; let client = TestClientBuilder::new_for_test() .signing_identity(identity.clone(), secret_key, cipher_suite) .build(); // TODO: Tests around extensions let key_package = client.generate_key_package_message().await.unwrap(); assert_eq!(key_package.version, protocol_version); let key_package = key_package.into_key_package().unwrap(); assert_eq!(key_package.cipher_suite, cipher_suite); assert_eq!( &key_package.leaf_node.signing_identity.credential, &get_test_basic_credential(b"foo".to_vec()) ); assert_eq!(key_package.leaf_node.signing_identity, identity); let capabilities = key_package.leaf_node.ungreased_capabilities(); assert_eq!(capabilities, client.config.capabilities()); let client_lifetime = client.config.lifetime(); 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)); } } #[cfg(feature = "by_ref_proposal")] #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn new_member_add_proposal_adds_to_group() { let mut alice_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; let (bob_identity, secret_key) = get_test_signing_identity(TEST_CIPHER_SUITE, b"bob").await; let bob = TestClientBuilder::new_for_test() .signing_identity(bob_identity.clone(), secret_key, TEST_CIPHER_SUITE) .build(); let proposal = bob .external_add_proposal( &alice_group.group.group_info_message(true).await.unwrap(), None, vec![], ) .await .unwrap(); let message = alice_group .group .process_incoming_message(proposal) .await .unwrap(); assert_matches!( message, ReceivedMessage::Proposal(ProposalMessageDescription { proposal: Proposal::Add(p), ..} ) if p.key_package.leaf_node.signing_identity == bob_identity ); alice_group.group.commit(vec![]).await.unwrap(); alice_group.group.apply_pending_commit().await.unwrap(); // Check that the new member is in the group assert!(alice_group .group .roster() .members_iter() .any(|member| member.signing_identity == bob_identity)) } #[cfg(feature = "psk")] #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] async fn join_via_external_commit(do_remove: bool, with_psk: bool) -> Result<(), MlsError> { // An external commit cannot be the first commit in a group as it requires // interim_transcript_hash to be computed from the confirmed_transcript_hash and // confirmation_tag, which is not the case for the initial interim_transcript_hash. let psk = PreSharedKey::from(b"psk".to_vec()); let psk_id = ExternalPskId::new(b"psk id".to_vec()); let mut alice_group = test_group_custom_config(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, |c| { c.psk(psk_id.clone(), psk.clone()) }) .await; let (mut bob_group, _) = alice_group .join_with_custom_config("bob", false, |c| { c.0.psk_store.insert(psk_id.clone(), psk.clone()); }) .await .unwrap(); let group_info_msg = alice_group .group .group_info_message_allowing_ext_commit(true) .await .unwrap(); let new_client_id = if do_remove { "bob" } else { "charlie" }; let (new_client_identity, secret_key) = get_test_signing_identity(TEST_CIPHER_SUITE, new_client_id.as_bytes()).await; let new_client = TestClientBuilder::new_for_test() .psk(psk_id.clone(), psk) .signing_identity(new_client_identity.clone(), secret_key, TEST_CIPHER_SUITE) .build(); let mut builder = new_client.external_commit_builder().unwrap(); if do_remove { builder = builder.with_removal(1); } if with_psk { builder = builder.with_external_psk(psk_id); } let (new_group, external_commit) = builder.build(group_info_msg).await?; let num_members = if do_remove { 2 } else { 3 }; assert_eq!(new_group.roster().members_iter().count(), num_members); let _ = alice_group .group .process_incoming_message(external_commit.clone()) .await .unwrap(); let bob_current_epoch = bob_group.group.current_epoch(); let message = bob_group .group .process_incoming_message(external_commit) .await .unwrap(); assert!(alice_group.group.roster().members_iter().count() == num_members); if !do_remove { assert!(bob_group.group.roster().members_iter().count() == num_members); } else { // Bob was removed so his epoch must stay the same assert_eq!(bob_group.group.current_epoch(), bob_current_epoch); #[cfg(feature = "state_update")] assert_matches!(message, ReceivedMessage::Commit(desc) if !desc.state_update.active); #[cfg(not(feature = "state_update"))] assert_matches!(message, ReceivedMessage::Commit(_)); } // Comparing epoch authenticators is sufficient to check that members are in sync. assert_eq!( alice_group.group.epoch_authenticator().unwrap(), new_group.epoch_authenticator().unwrap() ); Ok(()) } #[cfg(feature = "psk")] #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn test_external_commit() { // New member can join join_via_external_commit(false, false).await.unwrap(); // New member can remove an old copy of themselves join_via_external_commit(true, false).await.unwrap(); // New member can inject a PSK join_via_external_commit(false, true).await.unwrap(); // All works together join_via_external_commit(true, true).await.unwrap(); } #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn creating_an_external_commit_requires_a_group_info_message() { let (alice_identity, secret_key) = get_test_signing_identity(TEST_CIPHER_SUITE, b"alice").await; let alice = TestClientBuilder::new_for_test() .signing_identity(alice_identity.clone(), secret_key, TEST_CIPHER_SUITE) .build(); let msg = alice.generate_key_package_message().await.unwrap(); let res = alice.commit_external(msg).await.map(|_| ()); assert_matches!(res, Err(MlsError::UnexpectedMessageType)); } #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] async fn external_commit_with_invalid_group_info_fails() { let mut alice_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; let mut bob_group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; bob_group.group.commit(vec![]).await.unwrap(); bob_group.group.apply_pending_commit().await.unwrap(); let group_info_msg = bob_group .group .group_info_message_allowing_ext_commit(true) .await .unwrap(); let (carol_identity, secret_key) = get_test_signing_identity(TEST_CIPHER_SUITE, b"carol").await; let carol = TestClientBuilder::new_for_test() .signing_identity(carol_identity, secret_key, TEST_CIPHER_SUITE) .build(); let (_, external_commit) = carol .external_commit_builder() .unwrap() .build(group_info_msg) .await .unwrap(); // If Carol tries to join Alice's group using the group info from Bob's group, that fails. let res = alice_group .group .process_incoming_message(external_commit) .await; assert_matches!(res, Err(_)); } #[test] fn builder_can_be_obtained_from_client_to_edit_properties_for_new_client() { let alice = TestClientBuilder::new_for_test() .extension_type(33.into()) .build(); let bob = alice.to_builder().extension_type(34.into()).build(); assert_eq!(bob.config.supported_extensions(), [33, 34].map(Into::into)); } }