| // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. |
| // Copyright by contributors to this project. |
| // SPDX-License-Identifier: (Apache-2.0 OR MIT) |
| |
| //! Definitions to build an [`ExternalClient`]. |
| //! |
| //! See [`ExternalClientBuilder`]. |
| |
| use crate::{ |
| crypto::SignaturePublicKey, |
| extension::ExtensionType, |
| external_client::{ExternalClient, ExternalClientConfig}, |
| group::{ |
| mls_rules::{DefaultMlsRules, MlsRules}, |
| proposal::ProposalType, |
| }, |
| identity::CredentialType, |
| protocol_version::ProtocolVersion, |
| tree_kem::Capabilities, |
| CryptoProvider, Sealed, |
| }; |
| use std::{ |
| collections::HashMap, |
| fmt::{self, Debug}, |
| }; |
| |
| /// Base client configuration type when instantiating `ExternalClientBuilder` |
| pub type ExternalBaseConfig = Config<Missing, DefaultMlsRules, Missing>; |
| |
| /// Builder for [`ExternalClient`] |
| /// |
| /// This is returned by [`ExternalClient::builder`] and allows to tweak settings the |
| /// `ExternalClient` will use. At a minimum, the builder must be told the [`CryptoProvider`] |
| /// and [`IdentityProvider`] to use. Other settings have default values. This |
| /// means that the following methods must be called before [`ExternalClientBuilder::build`]: |
| /// |
| /// - To specify the [`CryptoProvider`]: [`ExternalClientBuilder::crypto_provider`] |
| /// - To specify the [`IdentityProvider`]: [`ExternalClientBuilder::identity_provider`] |
| /// |
| /// # Example |
| /// |
| /// ``` |
| /// use mls_rs::{ |
| /// external_client::ExternalClient, |
| /// identity::basic::BasicIdentityProvider, |
| /// }; |
| /// |
| /// use mls_rs_crypto_openssl::OpensslCryptoProvider; |
| /// |
| /// let _client = ExternalClient::builder() |
| /// .crypto_provider(OpensslCryptoProvider::default()) |
| /// .identity_provider(BasicIdentityProvider::new()) |
| /// .build(); |
| /// ``` |
| /// |
| /// # Spelling out an `ExternalClient` type |
| /// |
| /// There are two main ways to spell out an `ExternalClient` type if needed (e.g. function return type). |
| /// |
| /// The first option uses `impl MlsConfig`: |
| /// ``` |
| /// use mls_rs::{ |
| /// external_client::{ExternalClient, builder::MlsConfig}, |
| /// identity::basic::BasicIdentityProvider, |
| /// }; |
| /// |
| /// use mls_rs_crypto_openssl::OpensslCryptoProvider; |
| /// |
| /// fn make_client() -> ExternalClient<impl MlsConfig> { |
| /// ExternalClient::builder() |
| /// .crypto_provider(OpensslCryptoProvider::default()) |
| /// .identity_provider(BasicIdentityProvider::new()) |
| /// .build() |
| /// } |
| ///``` |
| /// |
| /// The second option is more verbose and consists in writing the full `ExternalClient` type: |
| /// ``` |
| /// use mls_rs::{ |
| /// external_client::{ExternalClient, builder::{ExternalBaseConfig, WithIdentityProvider, WithCryptoProvider}}, |
| /// identity::basic::BasicIdentityProvider, |
| /// }; |
| /// |
| /// use mls_rs_crypto_openssl::OpensslCryptoProvider; |
| /// |
| /// type MlsClient = ExternalClient<WithIdentityProvider< |
| /// BasicIdentityProvider, |
| /// WithCryptoProvider<OpensslCryptoProvider, ExternalBaseConfig>, |
| /// >>; |
| /// |
| /// fn make_client_2() -> MlsClient { |
| /// ExternalClient::builder() |
| /// .crypto_provider(OpensslCryptoProvider::new()) |
| /// .identity_provider(BasicIdentityProvider::new()) |
| /// .build() |
| /// } |
| /// |
| /// ``` |
| #[derive(Debug)] |
| pub struct ExternalClientBuilder<C>(C); |
| |
| impl Default for ExternalClientBuilder<ExternalBaseConfig> { |
| fn default() -> Self { |
| Self::new() |
| } |
| } |
| |
| impl ExternalClientBuilder<ExternalBaseConfig> { |
| pub fn new() -> Self { |
| Self(Config(ConfigInner { |
| settings: Default::default(), |
| identity_provider: Missing, |
| mls_rules: DefaultMlsRules::new(), |
| crypto_provider: Missing, |
| signing_data: None, |
| })) |
| } |
| } |
| |
| impl<C: IntoConfig> ExternalClientBuilder<C> { |
| /// Add an extension type to the list of extension types supported by the client. |
| pub fn extension_type( |
| self, |
| type_: ExtensionType, |
| ) -> ExternalClientBuilder<IntoConfigOutput<C>> { |
| self.extension_types(Some(type_)) |
| } |
| |
| /// Add multiple extension types to the list of extension types supported by the client. |
| pub fn extension_types<I>(self, types: I) -> ExternalClientBuilder<IntoConfigOutput<C>> |
| where |
| I: IntoIterator<Item = ExtensionType>, |
| { |
| let mut c = self.0.into_config(); |
| c.0.settings.extension_types.extend(types); |
| ExternalClientBuilder(c) |
| } |
| |
| /// Add a custom proposal type to the list of proposals types supported by the client. |
| pub fn custom_proposal_type( |
| self, |
| type_: ProposalType, |
| ) -> ExternalClientBuilder<IntoConfigOutput<C>> { |
| self.custom_proposal_types(Some(type_)) |
| } |
| |
| /// Add multiple custom proposal types to the list of proposal types supported by the client. |
| pub fn custom_proposal_types<I>(self, types: I) -> ExternalClientBuilder<IntoConfigOutput<C>> |
| where |
| I: IntoIterator<Item = ProposalType>, |
| { |
| let mut c = self.0.into_config(); |
| c.0.settings.custom_proposal_types.extend(types); |
| ExternalClientBuilder(c) |
| } |
| |
| /// Add a protocol version to the list of protocol versions supported by the client. |
| /// |
| /// If no protocol version is explicitly added, the client will support all protocol versions |
| /// supported by this crate. |
| pub fn protocol_version( |
| self, |
| version: ProtocolVersion, |
| ) -> ExternalClientBuilder<IntoConfigOutput<C>> { |
| self.protocol_versions(Some(version)) |
| } |
| |
| /// Add multiple protocol versions to the list of protocol versions supported by the client. |
| /// |
| /// If no protocol version is explicitly added, the client will support all protocol versions |
| /// supported by this crate. |
| pub fn protocol_versions<I>(self, versions: I) -> ExternalClientBuilder<IntoConfigOutput<C>> |
| where |
| I: IntoIterator<Item = ProtocolVersion>, |
| { |
| let mut c = self.0.into_config(); |
| c.0.settings.protocol_versions.extend(versions); |
| ExternalClientBuilder(c) |
| } |
| |
| /// Add an external signing key to be used by the client. |
| pub fn external_signing_key( |
| self, |
| id: Vec<u8>, |
| key: SignaturePublicKey, |
| ) -> ExternalClientBuilder<IntoConfigOutput<C>> { |
| let mut c = self.0.into_config(); |
| c.0.settings.external_signing_keys.insert(id, key); |
| ExternalClientBuilder(c) |
| } |
| |
| /// Specify the number of epochs before the current one to keep. |
| /// |
| /// By default, all epochs are kept. |
| pub fn max_epoch_jitter(self, max_jitter: u64) -> ExternalClientBuilder<IntoConfigOutput<C>> { |
| let mut c = self.0.into_config(); |
| c.0.settings.max_epoch_jitter = Some(max_jitter); |
| ExternalClientBuilder(c) |
| } |
| |
| /// Specify whether processed proposals should be cached by the external group. In case they |
| /// are not cached by the group, they should be cached externally and inserted using |
| /// `ExternalGroup::insert_proposal` before processing the next commit. |
| pub fn cache_proposals( |
| self, |
| cache_proposals: bool, |
| ) -> ExternalClientBuilder<IntoConfigOutput<C>> { |
| let mut c = self.0.into_config(); |
| c.0.settings.cache_proposals = cache_proposals; |
| ExternalClientBuilder(c) |
| } |
| |
| /// Set the identity validator to be used by the client. |
| pub fn identity_provider<I>( |
| self, |
| identity_provider: I, |
| ) -> ExternalClientBuilder<WithIdentityProvider<I, C>> |
| where |
| I: IdentityProvider, |
| { |
| let Config(c) = self.0.into_config(); |
| ExternalClientBuilder(Config(ConfigInner { |
| settings: c.settings, |
| identity_provider, |
| mls_rules: c.mls_rules, |
| crypto_provider: c.crypto_provider, |
| signing_data: c.signing_data, |
| })) |
| } |
| |
| /// Set the crypto provider to be used by the client. |
| /// |
| // TODO add a comment once we have a default provider |
| pub fn crypto_provider<Cp>( |
| self, |
| crypto_provider: Cp, |
| ) -> ExternalClientBuilder<WithCryptoProvider<Cp, C>> |
| where |
| Cp: CryptoProvider, |
| { |
| let Config(c) = self.0.into_config(); |
| ExternalClientBuilder(Config(ConfigInner { |
| settings: c.settings, |
| identity_provider: c.identity_provider, |
| mls_rules: c.mls_rules, |
| crypto_provider, |
| signing_data: c.signing_data, |
| })) |
| } |
| |
| /// Set the user-defined proposal rules to be used by the client. |
| /// |
| /// User-defined rules are used when sending and receiving commits before |
| /// enforcing general MLS protocol rules. If the rule set returns an error when |
| /// receiving a commit, the entire commit is considered invalid. If the |
| /// rule set would return an error when sending a commit, individual proposals |
| /// may be filtered out to compensate. |
| pub fn mls_rules<Pr>(self, mls_rules: Pr) -> ExternalClientBuilder<WithMlsRules<Pr, C>> |
| where |
| Pr: MlsRules, |
| { |
| let Config(c) = self.0.into_config(); |
| ExternalClientBuilder(Config(ConfigInner { |
| settings: c.settings, |
| identity_provider: c.identity_provider, |
| mls_rules, |
| crypto_provider: c.crypto_provider, |
| signing_data: c.signing_data, |
| })) |
| } |
| |
| /// Set the signature secret key used by the client to send external proposals. |
| pub fn signer( |
| self, |
| signer: SignatureSecretKey, |
| signing_identity: SigningIdentity, |
| ) -> ExternalClientBuilder<IntoConfigOutput<C>> { |
| let mut c = self.0.into_config(); |
| c.0.signing_data = Some((signer, signing_identity)); |
| ExternalClientBuilder(c) |
| } |
| } |
| |
| impl<C: IntoConfig> ExternalClientBuilder<C> |
| where |
| C::IdentityProvider: IdentityProvider + Clone, |
| C::MlsRules: MlsRules + Clone, |
| C::CryptoProvider: CryptoProvider + Clone, |
| { |
| pub(crate) fn build_config(self) -> IntoConfigOutput<C> { |
| let mut c = self.0.into_config(); |
| |
| if c.0.settings.protocol_versions.is_empty() { |
| c.0.settings.protocol_versions = ProtocolVersion::all().collect(); |
| } |
| |
| c |
| } |
| |
| /// Build an external client. |
| /// |
| /// See [`ExternalClientBuilder`] documentation if the return type of this function needs to be |
| /// spelled out. |
| pub fn build(self) -> ExternalClient<IntoConfigOutput<C>> { |
| let mut c = self.build_config(); |
| let signing_data = c.0.signing_data.take(); |
| ExternalClient::new(c, signing_data) |
| } |
| } |
| |
| /// Marker type for required `ExternalClientBuilder` services that have not been specified yet. |
| #[derive(Debug)] |
| pub struct Missing; |
| |
| /// Change the identity validator used by a client configuration. |
| /// |
| /// See [`ExternalClientBuilder::identity_provider`]. |
| pub type WithIdentityProvider<I, C> = |
| Config<I, <C as IntoConfig>::MlsRules, <C as IntoConfig>::CryptoProvider>; |
| |
| /// Change the proposal filter used by a client configuration. |
| /// |
| /// See [`ExternalClientBuilder::mls_rules`]. |
| pub type WithMlsRules<Pr, C> = |
| Config<<C as IntoConfig>::IdentityProvider, Pr, <C as IntoConfig>::CryptoProvider>; |
| |
| /// Change the crypto provider used by a client configuration. |
| /// |
| /// See [`ExternalClientBuilder::crypto_provider`]. |
| pub type WithCryptoProvider<Cp, C> = |
| Config<<C as IntoConfig>::IdentityProvider, <C as IntoConfig>::MlsRules, Cp>; |
| |
| /// Helper alias for `Config`. |
| pub type IntoConfigOutput<C> = Config< |
| <C as IntoConfig>::IdentityProvider, |
| <C as IntoConfig>::MlsRules, |
| <C as IntoConfig>::CryptoProvider, |
| >; |
| |
| impl<Ip, Pr, Cp> ExternalClientConfig for ConfigInner<Ip, Pr, Cp> |
| where |
| Ip: IdentityProvider + Clone, |
| Pr: MlsRules + Clone, |
| Cp: CryptoProvider + Clone, |
| { |
| type IdentityProvider = Ip; |
| type MlsRules = Pr; |
| type CryptoProvider = Cp; |
| |
| fn supported_extensions(&self) -> Vec<ExtensionType> { |
| self.settings.extension_types.clone() |
| } |
| |
| fn supported_protocol_versions(&self) -> Vec<ProtocolVersion> { |
| self.settings.protocol_versions.clone() |
| } |
| |
| fn identity_provider(&self) -> Self::IdentityProvider { |
| self.identity_provider.clone() |
| } |
| |
| fn crypto_provider(&self) -> Self::CryptoProvider { |
| self.crypto_provider.clone() |
| } |
| |
| fn external_signing_key(&self, external_key_id: &[u8]) -> Option<SignaturePublicKey> { |
| self.settings |
| .external_signing_keys |
| .get(external_key_id) |
| .cloned() |
| } |
| |
| fn mls_rules(&self) -> Self::MlsRules { |
| self.mls_rules.clone() |
| } |
| |
| fn max_epoch_jitter(&self) -> Option<u64> { |
| self.settings.max_epoch_jitter |
| } |
| |
| fn cache_proposals(&self) -> bool { |
| self.settings.cache_proposals |
| } |
| |
| fn supported_custom_proposals(&self) -> Vec<ProposalType> { |
| self.settings.custom_proposal_types.clone() |
| } |
| } |
| |
| impl<Ip, Mpf, Cp> Sealed for Config<Ip, Mpf, Cp> {} |
| |
| impl<Ip, Pr, Cp> MlsConfig for Config<Ip, Pr, Cp> |
| where |
| Ip: IdentityProvider + Clone, |
| Pr: MlsRules + Clone, |
| Cp: CryptoProvider + Clone, |
| { |
| type Output = ConfigInner<Ip, Pr, Cp>; |
| |
| fn get(&self) -> &Self::Output { |
| &self.0 |
| } |
| } |
| |
| /// Helper trait to allow consuming crates to easily write an external client type as |
| /// `ExternalClient<impl MlsConfig>` |
| /// |
| /// It is not meant to be implemented by consuming crates. `T: MlsConfig` implies |
| /// `T: ExternalClientConfig`. |
| pub trait MlsConfig: Send + Sync + Clone + Sealed { |
| #[doc(hidden)] |
| type Output: ExternalClientConfig; |
| |
| #[doc(hidden)] |
| fn get(&self) -> &Self::Output; |
| } |
| |
| /// Blanket implementation so that `T: MlsConfig` implies `T: ExternalClientConfig` |
| impl<T: MlsConfig> ExternalClientConfig for T { |
| type IdentityProvider = <T::Output as ExternalClientConfig>::IdentityProvider; |
| type MlsRules = <T::Output as ExternalClientConfig>::MlsRules; |
| type CryptoProvider = <T::Output as ExternalClientConfig>::CryptoProvider; |
| |
| fn supported_extensions(&self) -> Vec<ExtensionType> { |
| self.get().supported_extensions() |
| } |
| |
| fn supported_protocol_versions(&self) -> Vec<ProtocolVersion> { |
| self.get().supported_protocol_versions() |
| } |
| |
| fn supported_custom_proposals(&self) -> Vec<ProposalType> { |
| self.get().supported_custom_proposals() |
| } |
| |
| fn identity_provider(&self) -> Self::IdentityProvider { |
| self.get().identity_provider() |
| } |
| |
| fn crypto_provider(&self) -> Self::CryptoProvider { |
| self.get().crypto_provider() |
| } |
| |
| fn external_signing_key(&self, external_key_id: &[u8]) -> Option<SignaturePublicKey> { |
| self.get().external_signing_key(external_key_id) |
| } |
| |
| fn mls_rules(&self) -> Self::MlsRules { |
| self.get().mls_rules() |
| } |
| |
| fn cache_proposals(&self) -> bool { |
| self.get().cache_proposals() |
| } |
| |
| fn max_epoch_jitter(&self) -> Option<u64> { |
| self.get().max_epoch_jitter() |
| } |
| |
| fn capabilities(&self) -> Capabilities { |
| self.get().capabilities() |
| } |
| |
| fn version_supported(&self, version: ProtocolVersion) -> bool { |
| self.get().version_supported(version) |
| } |
| |
| fn supported_credentials(&self) -> Vec<CredentialType> { |
| self.get().supported_credentials() |
| } |
| } |
| |
| #[derive(Clone)] |
| pub(crate) struct Settings { |
| pub(crate) extension_types: Vec<ExtensionType>, |
| pub(crate) custom_proposal_types: Vec<ProposalType>, |
| pub(crate) protocol_versions: Vec<ProtocolVersion>, |
| pub(crate) external_signing_keys: HashMap<Vec<u8>, SignaturePublicKey>, |
| pub(crate) max_epoch_jitter: Option<u64>, |
| pub(crate) cache_proposals: bool, |
| } |
| |
| impl Debug for Settings { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| f.debug_struct("Settings") |
| .field("extension_types", &self.extension_types) |
| .field("custom_proposal_types", &self.custom_proposal_types) |
| .field("protocol_versions", &self.protocol_versions) |
| .field( |
| "external_signing_keys", |
| &mls_rs_core::debug::pretty_with(|f| { |
| f.debug_map() |
| .entries( |
| self.external_signing_keys |
| .iter() |
| .map(|(k, v)| (mls_rs_core::debug::pretty_bytes(k), v)), |
| ) |
| .finish() |
| }), |
| ) |
| .field("max_epoch_jitter", &self.max_epoch_jitter) |
| .field("cache_proposals", &self.cache_proposals) |
| .finish() |
| } |
| } |
| |
| impl Default for Settings { |
| fn default() -> Self { |
| Self { |
| cache_proposals: true, |
| extension_types: vec![], |
| protocol_versions: vec![], |
| external_signing_keys: Default::default(), |
| max_epoch_jitter: None, |
| custom_proposal_types: vec![], |
| } |
| } |
| } |
| |
| /// Definitions meant to be private that are inaccessible outside this crate. They need to be marked |
| /// `pub` because they appear in public definitions. |
| mod private { |
| use mls_rs_core::{crypto::SignatureSecretKey, identity::SigningIdentity}; |
| |
| use super::{IntoConfigOutput, Settings}; |
| |
| #[derive(Clone, Debug)] |
| pub struct Config<Ip, Pr, Cp>(pub(crate) ConfigInner<Ip, Pr, Cp>); |
| |
| #[derive(Clone, Debug)] |
| pub struct ConfigInner<Ip, Mpf, Cp> { |
| pub(crate) settings: Settings, |
| pub(crate) identity_provider: Ip, |
| pub(crate) mls_rules: Mpf, |
| pub(crate) crypto_provider: Cp, |
| pub(crate) signing_data: Option<(SignatureSecretKey, SigningIdentity)>, |
| } |
| |
| pub trait IntoConfig { |
| type IdentityProvider; |
| type MlsRules; |
| type CryptoProvider; |
| |
| fn into_config(self) -> IntoConfigOutput<Self>; |
| } |
| |
| impl<Ip, Pr, Cp> IntoConfig for Config<Ip, Pr, Cp> { |
| type IdentityProvider = Ip; |
| type MlsRules = Pr; |
| type CryptoProvider = Cp; |
| |
| fn into_config(self) -> Self { |
| self |
| } |
| } |
| } |
| |
| use mls_rs_core::{ |
| crypto::SignatureSecretKey, |
| identity::{IdentityProvider, SigningIdentity}, |
| }; |
| use private::{Config, ConfigInner, IntoConfig}; |
| |
| #[cfg(test)] |
| pub(crate) mod test_utils { |
| use crate::{ |
| cipher_suite::CipherSuite, crypto::test_utils::TestCryptoProvider, |
| identity::basic::BasicIdentityProvider, |
| }; |
| |
| use super::{ |
| ExternalBaseConfig, ExternalClientBuilder, WithCryptoProvider, WithIdentityProvider, |
| }; |
| |
| pub type TestExternalClientConfig = WithIdentityProvider< |
| BasicIdentityProvider, |
| WithCryptoProvider<TestCryptoProvider, ExternalBaseConfig>, |
| >; |
| |
| pub type TestExternalClientBuilder = ExternalClientBuilder<TestExternalClientConfig>; |
| |
| impl TestExternalClientBuilder { |
| pub fn new_for_test() -> Self { |
| ExternalClientBuilder::new() |
| .crypto_provider(TestCryptoProvider::default()) |
| .identity_provider(BasicIdentityProvider::new()) |
| } |
| |
| pub fn new_for_test_disabling_cipher_suite(cipher_suite: CipherSuite) -> Self { |
| let crypto_provider = TestCryptoProvider::with_enabled_cipher_suites( |
| TestCryptoProvider::all_supported_cipher_suites() |
| .into_iter() |
| .filter(|cs| cs != &cipher_suite) |
| .collect(), |
| ); |
| |
| ExternalClientBuilder::new() |
| .crypto_provider(crypto_provider) |
| .identity_provider(BasicIdentityProvider::new()) |
| } |
| } |
| } |