| // 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)); |
| } |
| } |