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