mls_rs/external_client/
builder.rs

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//! Definitions to build an [`ExternalClient`].
6//!
7//! See [`ExternalClientBuilder`].
8
9use crate::{
10    crypto::SignaturePublicKey,
11    extension::ExtensionType,
12    external_client::{ExternalClient, ExternalClientConfig},
13    group::{
14        mls_rules::{DefaultMlsRules, MlsRules},
15        proposal::ProposalType,
16    },
17    protocol_version::ProtocolVersion,
18    CryptoProvider, Sealed,
19};
20use std::{
21    collections::HashMap,
22    fmt::{self, Debug},
23};
24
25/// Base client configuration type when instantiating `ExternalClientBuilder`
26pub type ExternalBaseConfig = Config<Missing, DefaultMlsRules, Missing>;
27
28/// Builder for [`ExternalClient`]
29///
30/// This is returned by [`ExternalClient::builder`] and allows to tweak settings the
31/// `ExternalClient` will use. At a minimum, the builder must be told the [`CryptoProvider`]
32/// and [`IdentityProvider`] to use. Other settings have default values. This
33/// means that the following methods must be called before [`ExternalClientBuilder::build`]:
34///
35/// - To specify the [`CryptoProvider`]: [`ExternalClientBuilder::crypto_provider`]
36/// - To specify the [`IdentityProvider`]: [`ExternalClientBuilder::identity_provider`]
37///
38/// # Example
39///
40/// ```
41/// use mls_rs::{
42///     external_client::ExternalClient,
43///     identity::basic::BasicIdentityProvider,
44/// };
45///
46/// use mls_rs_crypto_openssl::OpensslCryptoProvider;
47///
48/// let _client = ExternalClient::builder()
49///     .crypto_provider(OpensslCryptoProvider::default())
50///     .identity_provider(BasicIdentityProvider::new())
51///     .build();
52/// ```
53///
54/// # Spelling out an `ExternalClient` type
55///
56/// There are two main ways to spell out an `ExternalClient` type if needed (e.g. function return type).
57///
58/// The first option uses `impl MlsConfig`:
59/// ```
60/// use mls_rs::{
61///     external_client::{ExternalClient, builder::MlsConfig},
62///     identity::basic::BasicIdentityProvider,
63/// };
64///
65/// use mls_rs_crypto_openssl::OpensslCryptoProvider;
66///
67/// fn make_client() -> ExternalClient<impl MlsConfig> {
68///     ExternalClient::builder()
69///         .crypto_provider(OpensslCryptoProvider::default())
70///         .identity_provider(BasicIdentityProvider::new())
71///         .build()
72/// }
73///```
74///
75/// The second option is more verbose and consists in writing the full `ExternalClient` type:
76/// ```
77/// use mls_rs::{
78///     external_client::{ExternalClient, builder::{ExternalBaseConfig, WithIdentityProvider, WithCryptoProvider}},
79///     identity::basic::BasicIdentityProvider,
80/// };
81///
82/// use mls_rs_crypto_openssl::OpensslCryptoProvider;
83///
84/// type MlsClient = ExternalClient<WithIdentityProvider<
85///     BasicIdentityProvider,
86///     WithCryptoProvider<OpensslCryptoProvider, ExternalBaseConfig>,
87/// >>;
88///
89/// fn make_client_2() -> MlsClient {
90///     ExternalClient::builder()
91///         .crypto_provider(OpensslCryptoProvider::new())
92///         .identity_provider(BasicIdentityProvider::new())
93///         .build()
94/// }
95///
96/// ```
97#[derive(Debug)]
98pub struct ExternalClientBuilder<C>(C);
99
100impl Default for ExternalClientBuilder<ExternalBaseConfig> {
101    fn default() -> Self {
102        Self::new()
103    }
104}
105
106impl ExternalClientBuilder<ExternalBaseConfig> {
107    pub fn new() -> Self {
108        Self(Config(ConfigInner {
109            settings: Default::default(),
110            identity_provider: Missing,
111            mls_rules: DefaultMlsRules::new(),
112            crypto_provider: Missing,
113            signing_data: None,
114        }))
115    }
116}
117
118impl<C: IntoConfig> ExternalClientBuilder<C> {
119    /// Add an extension type to the list of extension types supported by the client.
120    pub fn extension_type(
121        self,
122        type_: ExtensionType,
123    ) -> ExternalClientBuilder<IntoConfigOutput<C>> {
124        self.extension_types(Some(type_))
125    }
126
127    /// Add multiple extension types to the list of extension types supported by the client.
128    pub fn extension_types<I>(self, types: I) -> ExternalClientBuilder<IntoConfigOutput<C>>
129    where
130        I: IntoIterator<Item = ExtensionType>,
131    {
132        let mut c = self.0.into_config();
133        c.0.settings.extension_types.extend(types);
134        ExternalClientBuilder(c)
135    }
136
137    /// Add multiple custom proposal types to the list of proposal types supported by the client.
138    pub fn custom_proposal_types<I>(self, types: I) -> ExternalClientBuilder<IntoConfigOutput<C>>
139    where
140        I: IntoIterator<Item = ProposalType>,
141    {
142        let mut c = self.0.into_config();
143        c.0.settings.custom_proposal_types.extend(types);
144        ExternalClientBuilder(c)
145    }
146
147    /// Add a protocol version to the list of protocol versions supported by the client.
148    ///
149    /// If no protocol version is explicitly added, the client will support all protocol versions
150    /// supported by this crate.
151    pub fn protocol_version(
152        self,
153        version: ProtocolVersion,
154    ) -> ExternalClientBuilder<IntoConfigOutput<C>> {
155        self.protocol_versions(Some(version))
156    }
157
158    /// Add multiple protocol versions to the list of protocol versions supported by the client.
159    ///
160    /// If no protocol version is explicitly added, the client will support all protocol versions
161    /// supported by this crate.
162    pub fn protocol_versions<I>(self, versions: I) -> ExternalClientBuilder<IntoConfigOutput<C>>
163    where
164        I: IntoIterator<Item = ProtocolVersion>,
165    {
166        let mut c = self.0.into_config();
167        c.0.settings.protocol_versions.extend(versions);
168        ExternalClientBuilder(c)
169    }
170
171    /// Add an external signing key to be used by the client.
172    pub fn external_signing_key(
173        self,
174        id: Vec<u8>,
175        key: SignaturePublicKey,
176    ) -> ExternalClientBuilder<IntoConfigOutput<C>> {
177        let mut c = self.0.into_config();
178        c.0.settings.external_signing_keys.insert(id, key);
179        ExternalClientBuilder(c)
180    }
181
182    /// Specify the number of epochs before the current one to keep.
183    ///
184    /// By default, all epochs are kept.
185    pub fn max_epoch_jitter(self, max_jitter: u64) -> ExternalClientBuilder<IntoConfigOutput<C>> {
186        let mut c = self.0.into_config();
187        c.0.settings.max_epoch_jitter = Some(max_jitter);
188        ExternalClientBuilder(c)
189    }
190
191    /// Specify whether processed proposals should be cached by the external group. In case they
192    /// are not cached by the group, they should be cached externally and inserted using
193    /// `ExternalGroup::insert_proposal` before processing the next commit.
194    pub fn cache_proposals(
195        self,
196        cache_proposals: bool,
197    ) -> ExternalClientBuilder<IntoConfigOutput<C>> {
198        let mut c = self.0.into_config();
199        c.0.settings.cache_proposals = cache_proposals;
200        ExternalClientBuilder(c)
201    }
202
203    /// Set the identity validator to be used by the client.
204    pub fn identity_provider<I>(
205        self,
206        identity_provider: I,
207    ) -> ExternalClientBuilder<WithIdentityProvider<I, C>>
208    where
209        I: IdentityProvider,
210    {
211        let Config(c) = self.0.into_config();
212        ExternalClientBuilder(Config(ConfigInner {
213            settings: c.settings,
214            identity_provider,
215            mls_rules: c.mls_rules,
216            crypto_provider: c.crypto_provider,
217            signing_data: c.signing_data,
218        }))
219    }
220
221    /// Set the crypto provider to be used by the client.
222    ///
223    // TODO add a comment once we have a default provider
224    pub fn crypto_provider<Cp>(
225        self,
226        crypto_provider: Cp,
227    ) -> ExternalClientBuilder<WithCryptoProvider<Cp, C>>
228    where
229        Cp: CryptoProvider,
230    {
231        let Config(c) = self.0.into_config();
232        ExternalClientBuilder(Config(ConfigInner {
233            settings: c.settings,
234            identity_provider: c.identity_provider,
235            mls_rules: c.mls_rules,
236            crypto_provider,
237            signing_data: c.signing_data,
238        }))
239    }
240
241    /// Set the user-defined proposal rules to be used by the client.
242    ///
243    /// User-defined rules are used when sending and receiving commits before
244    /// enforcing general MLS protocol rules. If the rule set returns an error when
245    /// receiving a commit, the entire commit is considered invalid. If the
246    /// rule set would return an error when sending a commit, individual proposals
247    /// may be filtered out to compensate.
248    pub fn mls_rules<Pr>(self, mls_rules: Pr) -> ExternalClientBuilder<WithMlsRules<Pr, C>>
249    where
250        Pr: MlsRules,
251    {
252        let Config(c) = self.0.into_config();
253        ExternalClientBuilder(Config(ConfigInner {
254            settings: c.settings,
255            identity_provider: c.identity_provider,
256            mls_rules,
257            crypto_provider: c.crypto_provider,
258            signing_data: c.signing_data,
259        }))
260    }
261
262    /// Set the signature secret key used by the client to send external proposals.
263    pub fn signer(
264        self,
265        signer: SignatureSecretKey,
266        signing_identity: SigningIdentity,
267    ) -> ExternalClientBuilder<IntoConfigOutput<C>> {
268        let mut c = self.0.into_config();
269        c.0.signing_data = Some((signer, signing_identity));
270        ExternalClientBuilder(c)
271    }
272}
273
274impl<C: IntoConfig> ExternalClientBuilder<C>
275where
276    C::IdentityProvider: IdentityProvider + Clone,
277    C::MlsRules: MlsRules + Clone,
278    C::CryptoProvider: CryptoProvider + Clone,
279{
280    pub(crate) fn build_config(self) -> IntoConfigOutput<C> {
281        let mut c = self.0.into_config();
282
283        if c.0.settings.protocol_versions.is_empty() {
284            c.0.settings.protocol_versions = ProtocolVersion::all().collect();
285        }
286
287        c
288    }
289
290    /// Build an external client.
291    ///
292    /// See [`ExternalClientBuilder`] documentation if the return type of this function needs to be
293    /// spelled out.
294    pub fn build(self) -> ExternalClient<IntoConfigOutput<C>> {
295        let mut c = self.build_config();
296        let signing_data = c.0.signing_data.take();
297        ExternalClient::new(c, signing_data)
298    }
299}
300
301/// Marker type for required `ExternalClientBuilder` services that have not been specified yet.
302#[derive(Debug)]
303pub struct Missing;
304
305/// Change the identity validator used by a client configuration.
306///
307/// See [`ExternalClientBuilder::identity_provider`].
308pub type WithIdentityProvider<I, C> =
309    Config<I, <C as IntoConfig>::MlsRules, <C as IntoConfig>::CryptoProvider>;
310
311/// Change the proposal filter used by a client configuration.
312///
313/// See [`ExternalClientBuilder::mls_rules`].
314pub type WithMlsRules<Pr, C> =
315    Config<<C as IntoConfig>::IdentityProvider, Pr, <C as IntoConfig>::CryptoProvider>;
316
317/// Change the crypto provider used by a client configuration.
318///
319/// See [`ExternalClientBuilder::crypto_provider`].
320pub type WithCryptoProvider<Cp, C> =
321    Config<<C as IntoConfig>::IdentityProvider, <C as IntoConfig>::MlsRules, Cp>;
322
323/// Helper alias for `Config`.
324pub type IntoConfigOutput<C> = Config<
325    <C as IntoConfig>::IdentityProvider,
326    <C as IntoConfig>::MlsRules,
327    <C as IntoConfig>::CryptoProvider,
328>;
329
330impl<Ip, Pr, Cp> ExternalClientConfig for ConfigInner<Ip, Pr, Cp>
331where
332    Ip: IdentityProvider + Clone,
333    Pr: MlsRules + Clone,
334    Cp: CryptoProvider + Clone,
335{
336    type IdentityProvider = Ip;
337    type MlsRules = Pr;
338    type CryptoProvider = Cp;
339
340    fn supported_protocol_versions(&self) -> Vec<ProtocolVersion> {
341        self.settings.protocol_versions.clone()
342    }
343
344    fn identity_provider(&self) -> Self::IdentityProvider {
345        self.identity_provider.clone()
346    }
347
348    fn crypto_provider(&self) -> Self::CryptoProvider {
349        self.crypto_provider.clone()
350    }
351
352    fn external_signing_key(&self, external_key_id: &[u8]) -> Option<SignaturePublicKey> {
353        self.settings
354            .external_signing_keys
355            .get(external_key_id)
356            .cloned()
357    }
358
359    fn mls_rules(&self) -> Self::MlsRules {
360        self.mls_rules.clone()
361    }
362
363    fn max_epoch_jitter(&self) -> Option<u64> {
364        self.settings.max_epoch_jitter
365    }
366
367    fn cache_proposals(&self) -> bool {
368        self.settings.cache_proposals
369    }
370}
371
372impl<Ip, Mpf, Cp> Sealed for Config<Ip, Mpf, Cp> {}
373
374impl<Ip, Pr, Cp> MlsConfig for Config<Ip, Pr, Cp>
375where
376    Ip: IdentityProvider + Clone,
377    Pr: MlsRules + Clone,
378    Cp: CryptoProvider + Clone,
379{
380    type Output = ConfigInner<Ip, Pr, Cp>;
381
382    fn get(&self) -> &Self::Output {
383        &self.0
384    }
385}
386
387/// Helper trait to allow consuming crates to easily write an external client type as
388/// `ExternalClient<impl MlsConfig>`
389///
390/// It is not meant to be implemented by consuming crates. `T: MlsConfig` implies
391/// `T: ExternalClientConfig`.
392pub trait MlsConfig: Send + Sync + Clone + Sealed {
393    #[doc(hidden)]
394    type Output: ExternalClientConfig;
395
396    #[doc(hidden)]
397    fn get(&self) -> &Self::Output;
398}
399
400/// Blanket implementation so that `T: MlsConfig` implies `T: ExternalClientConfig`
401impl<T: MlsConfig> ExternalClientConfig for T {
402    type IdentityProvider = <T::Output as ExternalClientConfig>::IdentityProvider;
403    type MlsRules = <T::Output as ExternalClientConfig>::MlsRules;
404    type CryptoProvider = <T::Output as ExternalClientConfig>::CryptoProvider;
405
406    fn supported_protocol_versions(&self) -> Vec<ProtocolVersion> {
407        self.get().supported_protocol_versions()
408    }
409
410    fn identity_provider(&self) -> Self::IdentityProvider {
411        self.get().identity_provider()
412    }
413
414    fn crypto_provider(&self) -> Self::CryptoProvider {
415        self.get().crypto_provider()
416    }
417
418    fn external_signing_key(&self, external_key_id: &[u8]) -> Option<SignaturePublicKey> {
419        self.get().external_signing_key(external_key_id)
420    }
421
422    fn mls_rules(&self) -> Self::MlsRules {
423        self.get().mls_rules()
424    }
425
426    fn cache_proposals(&self) -> bool {
427        self.get().cache_proposals()
428    }
429
430    fn max_epoch_jitter(&self) -> Option<u64> {
431        self.get().max_epoch_jitter()
432    }
433
434    fn version_supported(&self, version: ProtocolVersion) -> bool {
435        self.get().version_supported(version)
436    }
437}
438
439#[derive(Clone)]
440pub(crate) struct Settings {
441    pub(crate) extension_types: Vec<ExtensionType>,
442    pub(crate) custom_proposal_types: Vec<ProposalType>,
443    pub(crate) protocol_versions: Vec<ProtocolVersion>,
444    pub(crate) external_signing_keys: HashMap<Vec<u8>, SignaturePublicKey>,
445    pub(crate) max_epoch_jitter: Option<u64>,
446    pub(crate) cache_proposals: bool,
447}
448
449impl Debug for Settings {
450    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
451        f.debug_struct("Settings")
452            .field("extension_types", &self.extension_types)
453            .field("custom_proposal_types", &self.custom_proposal_types)
454            .field("protocol_versions", &self.protocol_versions)
455            .field(
456                "external_signing_keys",
457                &mls_rs_core::debug::pretty_with(|f| {
458                    f.debug_map()
459                        .entries(
460                            self.external_signing_keys
461                                .iter()
462                                .map(|(k, v)| (mls_rs_core::debug::pretty_bytes(k), v)),
463                        )
464                        .finish()
465                }),
466            )
467            .field("max_epoch_jitter", &self.max_epoch_jitter)
468            .field("cache_proposals", &self.cache_proposals)
469            .finish()
470    }
471}
472
473impl Default for Settings {
474    fn default() -> Self {
475        Self {
476            cache_proposals: true,
477            extension_types: vec![],
478            protocol_versions: vec![],
479            external_signing_keys: Default::default(),
480            max_epoch_jitter: None,
481            custom_proposal_types: vec![],
482        }
483    }
484}
485
486/// Definitions meant to be private that are inaccessible outside this crate. They need to be marked
487/// `pub` because they appear in public definitions.
488mod private {
489    use mls_rs_core::{crypto::SignatureSecretKey, identity::SigningIdentity};
490
491    use super::{IntoConfigOutput, Settings};
492
493    #[derive(Clone, Debug)]
494    pub struct Config<Ip, Pr, Cp>(pub(crate) ConfigInner<Ip, Pr, Cp>);
495
496    #[derive(Clone, Debug)]
497    pub struct ConfigInner<Ip, Mpf, Cp> {
498        pub(crate) settings: Settings,
499        pub(crate) identity_provider: Ip,
500        pub(crate) mls_rules: Mpf,
501        pub(crate) crypto_provider: Cp,
502        pub(crate) signing_data: Option<(SignatureSecretKey, SigningIdentity)>,
503    }
504
505    pub trait IntoConfig {
506        type IdentityProvider;
507        type MlsRules;
508        type CryptoProvider;
509
510        fn into_config(self) -> IntoConfigOutput<Self>;
511    }
512
513    impl<Ip, Pr, Cp> IntoConfig for Config<Ip, Pr, Cp> {
514        type IdentityProvider = Ip;
515        type MlsRules = Pr;
516        type CryptoProvider = Cp;
517
518        fn into_config(self) -> Self {
519            self
520        }
521    }
522}
523
524use mls_rs_core::{
525    crypto::SignatureSecretKey,
526    identity::{IdentityProvider, SigningIdentity},
527};
528use private::{Config, ConfigInner, IntoConfig};
529
530#[cfg(test)]
531pub(crate) mod test_utils {
532    use crate::{
533        cipher_suite::CipherSuite, crypto::test_utils::TestCryptoProvider,
534        identity::basic::BasicIdentityProvider,
535    };
536
537    use super::{
538        ExternalBaseConfig, ExternalClientBuilder, WithCryptoProvider, WithIdentityProvider,
539    };
540
541    pub type TestExternalClientConfig = WithIdentityProvider<
542        BasicIdentityProvider,
543        WithCryptoProvider<TestCryptoProvider, ExternalBaseConfig>,
544    >;
545
546    pub type TestExternalClientBuilder = ExternalClientBuilder<TestExternalClientConfig>;
547
548    impl TestExternalClientBuilder {
549        pub fn new_for_test() -> Self {
550            ExternalClientBuilder::new()
551                .crypto_provider(TestCryptoProvider::new())
552                .identity_provider(BasicIdentityProvider::new())
553        }
554
555        pub fn new_for_test_disabling_cipher_suite(cipher_suite: CipherSuite) -> Self {
556            let crypto_provider = TestCryptoProvider::with_enabled_cipher_suites(
557                TestCryptoProvider::all_supported_cipher_suites()
558                    .into_iter()
559                    .filter(|cs| cs != &cipher_suite)
560                    .collect(),
561            );
562
563            ExternalClientBuilder::new()
564                .crypto_provider(crypto_provider)
565                .identity_provider(BasicIdentityProvider::new())
566        }
567    }
568}