core_crypto/mls/conversation/
config.rs

1//! Conversation configuration.
2//!
3//! Either use [MlsConversationConfiguration] when creating a conversation or [MlsCustomConfiguration]
4//! when joining one by Welcome or external commit
5
6use mls_crypto_provider::MlsCryptoProvider;
7use openmls::prelude::{
8    Capabilities, Credential, CredentialType, ExternalSender, OpenMlsSignaturePublicKey,
9    PURE_CIPHERTEXT_WIRE_FORMAT_POLICY, PURE_PLAINTEXT_WIRE_FORMAT_POLICY, ProtocolVersion,
10    RequiredCapabilitiesExtension, SenderRatchetConfiguration, WireFormatPolicy,
11};
12use openmls_traits::{
13    OpenMlsCryptoProvider,
14    crypto::OpenMlsCrypto,
15    types::{Ciphersuite, SignatureScheme},
16};
17use serde::{Deserialize, Serialize};
18use wire_e2e_identity::prelude::parse_json_jwk;
19
20use super::Result;
21use crate::{MlsError, RecursiveError, prelude::MlsCiphersuite};
22
23/// Sets the config in OpenMls for the oldest possible epoch(past current) that a message can be decrypted
24pub(crate) const MAX_PAST_EPOCHS: usize = 3;
25
26/// Window for which decryption secrets are kept within an epoch. Use this with caution since this affects forward secrecy within an epoch.
27/// Use this when the Delivery Service cannot guarantee application messages order
28pub(crate) const OUT_OF_ORDER_TOLERANCE: u32 = 2;
29
30/// How many application messages can be skipped. Use this when the Delivery Service can drop application messages
31pub(crate) const MAXIMUM_FORWARD_DISTANCE: u32 = 1000;
32
33/// The configuration parameters for a group/conversation
34#[derive(Debug, Clone, Default)]
35pub struct MlsConversationConfiguration {
36    /// The `OpenMls` Ciphersuite used in the group
37    pub ciphersuite: MlsCiphersuite,
38    /// Delivery service public signature key and credential
39    pub external_senders: Vec<ExternalSender>,
40    /// Implementation specific configuration
41    pub custom: MlsCustomConfiguration,
42}
43
44impl MlsConversationConfiguration {
45    const WIRE_SERVER_IDENTITY: &'static str = "wire-server";
46
47    const PADDING_SIZE: usize = 128;
48
49    /// Default protocol
50    pub(crate) const DEFAULT_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::Mls10;
51
52    /// List all until further notice
53    pub(crate) const DEFAULT_SUPPORTED_CREDENTIALS: &'static [CredentialType] =
54        &[CredentialType::Basic, CredentialType::X509];
55
56    /// Conservative sensible defaults
57    pub(crate) const DEFAULT_SUPPORTED_CIPHERSUITES: &'static [Ciphersuite] = &[
58        Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519,
59        Ciphersuite::MLS_128_DHKEMP256_AES128GCM_SHA256_P256,
60        Ciphersuite::MLS_128_DHKEMX25519_CHACHA20POLY1305_SHA256_Ed25519,
61        Ciphersuite::MLS_256_DHKEMP384_AES256GCM_SHA384_P384,
62        Ciphersuite::MLS_256_DHKEMP521_AES256GCM_SHA512_P521,
63    ];
64
65    /// Not used at the moment
66    const NUMBER_RESUMPTION_PSK: usize = 1;
67
68    /// Generates an `MlsGroupConfig` from this configuration
69    #[inline(always)]
70    pub fn as_openmls_default_configuration(&self) -> Result<openmls::group::MlsGroupConfig> {
71        let crypto_config = openmls::prelude::CryptoConfig {
72            version: Self::DEFAULT_PROTOCOL_VERSION,
73            ciphersuite: self.ciphersuite.into(),
74        };
75        Ok(openmls::group::MlsGroupConfig::builder()
76            .wire_format_policy(self.custom.wire_policy.into())
77            .max_past_epochs(MAX_PAST_EPOCHS)
78            .padding_size(Self::PADDING_SIZE)
79            .number_of_resumption_psks(Self::NUMBER_RESUMPTION_PSK)
80            .leaf_capabilities(Self::default_leaf_capabilities())
81            .required_capabilities(self.default_required_capabilities())
82            .sender_ratchet_configuration(SenderRatchetConfiguration::new(
83                self.custom.out_of_order_tolerance,
84                self.custom.maximum_forward_distance,
85            ))
86            .use_ratchet_tree_extension(true)
87            .external_senders(self.external_senders.clone())
88            .crypto_config(crypto_config)
89            .build())
90    }
91
92    /// Default capabilities for every generated [openmls::prelude::KeyPackage]
93    pub fn default_leaf_capabilities() -> Capabilities {
94        Capabilities::new(
95            Some(&[Self::DEFAULT_PROTOCOL_VERSION]),
96            Some(Self::DEFAULT_SUPPORTED_CIPHERSUITES),
97            Some(&[]),
98            Some(&[]),
99            Some(Self::DEFAULT_SUPPORTED_CREDENTIALS),
100        )
101    }
102
103    fn default_required_capabilities(&self) -> RequiredCapabilitiesExtension {
104        RequiredCapabilitiesExtension::new(&[], &[], Self::DEFAULT_SUPPORTED_CREDENTIALS)
105    }
106
107    /// This expects a raw json serialized JWK. It works with any Signature scheme
108    pub(crate) fn parse_external_sender(jwk: &[u8]) -> Result<ExternalSender> {
109        let pk = parse_json_jwk(jwk)
110            .map_err(wire_e2e_identity::prelude::E2eIdentityError::from)
111            .map_err(crate::e2e_identity::Error::from)
112            .map_err(RecursiveError::e2e_identity("parsing jwk"))?;
113        Ok(ExternalSender::new(
114            pk.into(),
115            Credential::new_basic(Self::WIRE_SERVER_IDENTITY.into()),
116        ))
117    }
118
119    /// This supports the legacy behaviour where the server was providing the external sender public key
120    /// raw.
121    // TODO: remove at some point when the backend API is not used anymore. Tracking issue: WPB-9614
122    pub(crate) fn legacy_external_sender(
123        key: Vec<u8>,
124        signature_scheme: SignatureScheme,
125        backend: &MlsCryptoProvider,
126    ) -> Result<ExternalSender> {
127        backend
128            .crypto()
129            .validate_signature_key(signature_scheme, &key[..])
130            .map_err(MlsError::wrap("validating signature key"))?;
131        let key = OpenMlsSignaturePublicKey::new(key.into(), signature_scheme)
132            .map_err(MlsError::wrap("creating new signature public key"))?;
133        Ok(ExternalSender::new(
134            key.into(),
135            Credential::new_basic(Self::WIRE_SERVER_IDENTITY.into()),
136        ))
137    }
138}
139
140/// The configuration parameters for a group/conversation which are not handled natively by openmls
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct MlsCustomConfiguration {
143    // TODO: Not implemented yet. Tracking issue: WPB-9609
144    /// Duration in seconds after which we will automatically force a self_update commit
145    pub key_rotation_span: Option<std::time::Duration>,
146    /// Defines if handshake messages are encrypted or not
147    pub wire_policy: MlsWirePolicy,
148    /// Window for which decryption secrets are kept within an epoch. Use this with caution since
149    /// this affects forward secrecy within an epoch. Use this when the Delivery Service cannot
150    /// guarantee application messages order.
151    pub out_of_order_tolerance: u32,
152    /// How many application messages can be skipped. Use this when the Delivery Service can drop
153    /// application messages
154    pub maximum_forward_distance: u32,
155}
156
157impl Default for MlsCustomConfiguration {
158    fn default() -> Self {
159        Self {
160            wire_policy: MlsWirePolicy::Plaintext,
161            key_rotation_span: Default::default(),
162            out_of_order_tolerance: OUT_OF_ORDER_TOLERANCE,
163            maximum_forward_distance: MAXIMUM_FORWARD_DISTANCE,
164        }
165    }
166}
167
168/// Wrapper over [WireFormatPolicy](openmls::prelude::WireFormatPolicy)
169#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Serialize, Deserialize)]
170#[repr(u8)]
171pub enum MlsWirePolicy {
172    /// Handshake messages are never encrypted
173    #[default]
174    Plaintext = 1,
175    /// Handshake messages are always encrypted
176    Ciphertext = 2,
177}
178
179impl From<MlsWirePolicy> for WireFormatPolicy {
180    fn from(policy: MlsWirePolicy) -> Self {
181        match policy {
182            MlsWirePolicy::Ciphertext => PURE_CIPHERTEXT_WIRE_FORMAT_POLICY,
183            MlsWirePolicy::Plaintext => PURE_PLAINTEXT_WIRE_FORMAT_POLICY,
184        }
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use openmls::prelude::ProtocolVersion;
191    use openmls_traits::{
192        OpenMlsCryptoProvider,
193        crypto::OpenMlsCrypto,
194        types::{SignatureScheme, VerifiableCiphersuite},
195    };
196    use wasm_bindgen_test::*;
197    use wire_e2e_identity::prelude::JwsAlgorithm;
198
199    use crate::mls::conversation::ConversationWithMls as _;
200    use crate::{prelude::MlsConversationConfiguration, test_utils::*};
201
202    wasm_bindgen_test_configure!(run_in_browser);
203
204    #[cfg_attr(not(target_family = "wasm"), async_std::test)]
205    #[wasm_bindgen_test]
206    pub async fn group_should_have_required_capabilities() {
207        let case = TestCase::default();
208        run_test_with_client_ids(case.clone(), ["alice"], move |[cc]| {
209            Box::pin(async move {
210                let id = conversation_id();
211                cc.context
212                    .new_conversation(&id, case.credential_type, case.cfg.clone())
213                    .await
214                    .unwrap();
215                let conv = cc.context.conversation(&id).await.unwrap();
216                let group = conv.conversation().await;
217
218                let capabilities = group.group.group_context_extensions().required_capabilities().unwrap();
219
220                // see https://www.rfc-editor.org/rfc/rfc9420.html#section-11.1
221                assert!(capabilities.extension_types().is_empty());
222                assert!(capabilities.proposal_types().is_empty());
223                assert_eq!(
224                    capabilities.credential_types(),
225                    MlsConversationConfiguration::DEFAULT_SUPPORTED_CREDENTIALS
226                );
227            })
228        })
229        .await
230    }
231
232    #[apply(all_cred_cipher)]
233    #[wasm_bindgen_test]
234    pub async fn creator_leaf_node_should_have_default_capabilities(case: TestCase) {
235        run_test_with_client_ids(case.clone(), ["alice"], move |[cc]| {
236            Box::pin(async move {
237                let id = conversation_id();
238                cc.context
239                    .new_conversation(&id, case.credential_type, case.cfg.clone())
240                    .await
241                    .unwrap();
242                let conv = cc.context.conversation(&id).await.unwrap();
243                let group = conv.conversation().await;
244
245                // verifying https://www.rfc-editor.org/rfc/rfc9420.html#section-7.2
246                let creator_capabilities = group.group.own_leaf().unwrap().capabilities();
247
248                // https://www.rfc-editor.org/rfc/rfc9420.html#section-7.2-5.1.1
249                // ProtocolVersion must be the default one
250                assert_eq!(creator_capabilities.versions(), &[ProtocolVersion::Mls10]);
251
252                // To prevent downgrade attacks, Ciphersuite MUST ONLY contain the current one
253                assert_eq!(
254                    creator_capabilities.ciphersuites().to_vec(),
255                    MlsConversationConfiguration::DEFAULT_SUPPORTED_CIPHERSUITES
256                        .iter()
257                        .map(|c| VerifiableCiphersuite::from(*c))
258                        .collect::<Vec<_>>()
259                );
260
261                // Proposals MUST be empty since we support all the default ones
262                assert!(creator_capabilities.proposals().is_empty());
263
264                // Extensions MUST only contain non-default extension (i.e. empty for now)
265                assert!(creator_capabilities.extensions().is_empty(),);
266
267                // To prevent downgrade attacks, Credentials should just contain the current
268                assert_eq!(
269                    creator_capabilities.credentials(),
270                    MlsConversationConfiguration::DEFAULT_SUPPORTED_CREDENTIALS
271                );
272            })
273        })
274        .await
275    }
276
277    #[apply(all_cred_cipher)]
278    #[wasm_bindgen_test]
279    pub async fn should_support_raw_external_sender(case: TestCase) {
280        run_test_with_client_ids(case.clone(), ["alice"], move |[cc]| {
281            Box::pin(async move {
282                let (_sk, pk) = cc
283                    .context
284                    .mls_provider()
285                    .await
286                    .unwrap()
287                    .crypto()
288                    .signature_key_gen(case.signature_scheme())
289                    .unwrap();
290
291                assert!(
292                    cc.context
293                        .set_raw_external_senders(&mut case.cfg.clone(), vec![pk])
294                        .await
295                        .is_ok()
296                );
297            })
298        })
299        .await
300    }
301
302    #[apply(all_cred_cipher)]
303    #[wasm_bindgen_test]
304    pub async fn should_support_jwk_external_sender(case: TestCase) {
305        run_test_with_client_ids(case.clone(), ["alice"], move |[cc]| {
306            Box::pin(async move {
307                let sc = case.signature_scheme();
308
309                let alg = match sc {
310                    SignatureScheme::ED25519 => JwsAlgorithm::Ed25519,
311                    SignatureScheme::ECDSA_SECP256R1_SHA256 => JwsAlgorithm::P256,
312                    SignatureScheme::ECDSA_SECP384R1_SHA384 => JwsAlgorithm::P384,
313                    SignatureScheme::ECDSA_SECP521R1_SHA512 => JwsAlgorithm::P521,
314                    SignatureScheme::ED448 => unreachable!(),
315                };
316
317                let jwk = wire_e2e_identity::prelude::generate_jwk(alg);
318                cc.context
319                    .set_raw_external_senders(&mut case.cfg.clone(), vec![jwk])
320                    .await
321                    .unwrap();
322            })
323        })
324        .await;
325    }
326}