core_crypto/mls/conversation/
config.rs

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