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