Skip to main content

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 openmls::prelude::{
7    Capabilities, Credential, CredentialType, ExternalSender, OpenMlsSignaturePublicKey,
8    PURE_CIPHERTEXT_WIRE_FORMAT_POLICY, PURE_PLAINTEXT_WIRE_FORMAT_POLICY, ProtocolVersion,
9    RequiredCapabilitiesExtension, SenderRatchetConfiguration, WireFormatPolicy,
10};
11use openmls_traits::{
12    crypto::OpenMlsCrypto,
13    types::{Ciphersuite as MlsCiphersuite, SignatureScheme},
14};
15use serde::{Deserialize, Serialize};
16use wire_e2e_identity::parse_json_jwk;
17
18use super::Result;
19use crate::{
20    CipherSuite, MlsError, RecursiveError, mls::conversation::ExternalSenderKey, mls_provider::MlsCryptoProvider,
21};
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
27/// secrecy within an epoch. 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: CipherSuite,
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 [MlsCiphersuite] = &[
58        MlsCiphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519,
59        MlsCiphersuite::MLS_128_DHKEMP256_AES128GCM_SHA256_P256,
60        MlsCiphersuite::MLS_128_DHKEMX25519_CHACHA20POLY1305_SHA256_Ed25519,
61        MlsCiphersuite::MLS_256_DHKEMP384_AES256GCM_SHA384_P384,
62        MlsCiphersuite::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    /// Parses external senders' keys provided by the delivery service
108    /// and updates the conversation's configuration with them.
109    pub async fn set_raw_external_senders(
110        &mut self,
111        mls_crypto_provider: &MlsCryptoProvider,
112        external_senders: impl IntoIterator<Item = ExternalSenderKey>,
113    ) -> Result<()> {
114        self.external_senders = external_senders
115            .into_iter()
116            .map(|key| {
117                MlsConversationConfiguration::parse_external_sender(&key).or_else(|_| {
118                    MlsConversationConfiguration::legacy_external_sender(
119                        key.into(),
120                        self.ciphersuite.signature_algorithm(),
121                        mls_crypto_provider,
122                    )
123                })
124            })
125            .collect::<crate::mls::conversation::Result<_>>()
126            .map_err(RecursiveError::mls_conversation("setting external sender"))?;
127        Ok(())
128    }
129
130    /// This expects a raw json serialized JWK. It works with any Signature scheme
131    pub(crate) fn parse_external_sender(jwk: &[u8]) -> Result<ExternalSender> {
132        let pk = parse_json_jwk(jwk)
133            .map_err(wire_e2e_identity::E2eIdentityError::from)
134            .map_err(RecursiveError::e2e_identity("parsing jwk"))?;
135        Ok(ExternalSender::new(
136            pk.into(),
137            Credential::new_basic(Self::WIRE_SERVER_IDENTITY.into()),
138        ))
139    }
140
141    /// This supports the legacy behaviour where the server was providing the external sender public key
142    /// raw.
143    // TODO: remove at some point when the backend API is not used anymore. Tracking issue: WPB-9614
144    pub(crate) fn legacy_external_sender(
145        key: Vec<u8>,
146        signature_scheme: SignatureScheme,
147        backend: &MlsCryptoProvider,
148    ) -> Result<ExternalSender> {
149        backend
150            .validate_signature_key(signature_scheme, &key[..])
151            .map_err(MlsError::wrap("validating signature key"))?;
152        let key = OpenMlsSignaturePublicKey::new(key.into(), signature_scheme)
153            .map_err(MlsError::wrap("creating new signature public key"))?;
154        Ok(ExternalSender::new(
155            key.into(),
156            Credential::new_basic(Self::WIRE_SERVER_IDENTITY.into()),
157        ))
158    }
159}
160
161/// The configuration parameters for a group/conversation which are not handled natively by openmls
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct MlsCustomConfiguration {
164    // TODO: Not implemented yet. Tracking issue: WPB-9609
165    /// Duration in seconds after which we will automatically force a self_update commit
166    pub key_rotation_span: Option<std::time::Duration>,
167    /// Defines if handshake messages are encrypted or not
168    pub wire_policy: MlsWirePolicy,
169    /// Window for which decryption secrets are kept within an epoch. Use this with caution since
170    /// this affects forward secrecy within an epoch. Use this when the Delivery Service cannot
171    /// guarantee application messages order.
172    pub out_of_order_tolerance: u32,
173    /// How many application messages can be skipped. Use this when the Delivery Service can drop
174    /// application messages
175    pub maximum_forward_distance: u32,
176}
177
178impl Default for MlsCustomConfiguration {
179    fn default() -> Self {
180        Self {
181            wire_policy: MlsWirePolicy::Plaintext,
182            key_rotation_span: Default::default(),
183            out_of_order_tolerance: OUT_OF_ORDER_TOLERANCE,
184            maximum_forward_distance: MAXIMUM_FORWARD_DISTANCE,
185        }
186    }
187}
188
189/// Wrapper over [WireFormatPolicy]
190#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Serialize, Deserialize)]
191#[repr(u8)]
192pub enum MlsWirePolicy {
193    /// Handshake messages are never encrypted
194    #[default]
195    Plaintext = 1,
196    /// Handshake messages are always encrypted
197    Ciphertext = 2,
198}
199
200impl From<MlsWirePolicy> for WireFormatPolicy {
201    fn from(policy: MlsWirePolicy) -> Self {
202        match policy {
203            MlsWirePolicy::Ciphertext => PURE_CIPHERTEXT_WIRE_FORMAT_POLICY,
204            MlsWirePolicy::Plaintext => PURE_PLAINTEXT_WIRE_FORMAT_POLICY,
205        }
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use openmls::prelude::ProtocolVersion;
212    use openmls_traits::{
213        OpenMlsCryptoProvider,
214        crypto::OpenMlsCrypto,
215        types::{SignatureScheme, VerifiableCiphersuite},
216    };
217    use wire_e2e_identity::JwsAlgorithm;
218
219    use crate::{MlsConversationConfiguration, mls::conversation::ConversationWithMls as _, test_utils::*};
220
221    #[macro_rules_attribute::apply(smol_macros::test)]
222    async fn group_should_have_required_capabilities() {
223        let case = TestContext::default();
224
225        let [session] = case.sessions().await;
226        Box::pin(async move {
227            let conversation = case.create_conversation([&session]).await;
228            let guard = conversation.guard().await;
229            let group = guard.conversation().await;
230
231            let capabilities = group.group.group_context_extensions().required_capabilities().unwrap();
232
233            // see https://www.rfc-editor.org/rfc/rfc9420.html#section-11.1
234            assert!(capabilities.extension_types().is_empty());
235            assert!(capabilities.proposal_types().is_empty());
236            assert_eq!(
237                capabilities.credential_types(),
238                MlsConversationConfiguration::DEFAULT_SUPPORTED_CREDENTIALS
239            );
240        })
241        .await
242    }
243
244    #[apply(all_cred_cipher)]
245    pub async fn creator_leaf_node_should_have_default_capabilities(case: TestContext) {
246        let [session] = case.sessions().await;
247        Box::pin(async move {
248            let conversation = case.create_conversation([&session]).await;
249            let guard = conversation.guard().await;
250            let group = guard.conversation().await;
251
252            // verifying https://www.rfc-editor.org/rfc/rfc9420.html#section-7.2
253            let creator_capabilities = group.group.own_leaf().unwrap().capabilities();
254
255            // https://www.rfc-editor.org/rfc/rfc9420.html#section-7.2-5.1.1
256            // ProtocolVersion must be the default one
257            assert_eq!(creator_capabilities.versions(), &[ProtocolVersion::Mls10]);
258
259            // To prevent downgrade attacks, Ciphersuite MUST ONLY contain the current one
260            assert_eq!(
261                creator_capabilities.ciphersuites().to_vec(),
262                MlsConversationConfiguration::DEFAULT_SUPPORTED_CIPHERSUITES
263                    .iter()
264                    .map(|c| VerifiableCiphersuite::from(*c))
265                    .collect::<Vec<_>>()
266            );
267
268            // Proposals MUST be empty since we support all the default ones
269            assert!(creator_capabilities.proposals().is_empty());
270
271            // Extensions MUST only contain non-default extension (i.e. empty for now)
272            assert!(creator_capabilities.extensions().is_empty(),);
273
274            // To prevent downgrade attacks, Credentials should just contain the current
275            assert_eq!(
276                creator_capabilities.credentials(),
277                MlsConversationConfiguration::DEFAULT_SUPPORTED_CREDENTIALS
278            );
279        })
280        .await
281    }
282
283    #[apply(all_cred_cipher)]
284    pub async fn should_support_raw_external_sender(case: TestContext) {
285        let [cc] = case.sessions().await;
286        Box::pin(async move {
287            let (_sk, pk) = cc
288                .transaction
289                .mls_provider()
290                .await
291                .unwrap()
292                .crypto()
293                .signature_key_gen(case.signature_scheme())
294                .unwrap();
295            let pk = pk.into();
296
297            assert!(
298                case.cfg
299                    .clone()
300                    .set_raw_external_senders(&cc.session().await.crypto_provider, vec![pk])
301                    .await
302                    .is_ok()
303            );
304        })
305        .await
306    }
307
308    #[apply(all_cred_cipher)]
309    pub async fn should_support_jwk_external_sender(case: TestContext) {
310        let [cc] = case.sessions().await;
311        Box::pin(async move {
312            let sc = case.signature_scheme();
313
314            let alg = match sc {
315                SignatureScheme::ED25519 => JwsAlgorithm::Ed25519,
316                SignatureScheme::ECDSA_SECP256R1_SHA256 => JwsAlgorithm::P256,
317                SignatureScheme::ECDSA_SECP384R1_SHA384 => JwsAlgorithm::P384,
318                SignatureScheme::ECDSA_SECP521R1_SHA512 => JwsAlgorithm::P521,
319                SignatureScheme::ED448 => unreachable!(),
320            };
321
322            let jwk = wire_e2e_identity::generate_jwk(alg).into();
323            assert!(
324                case.cfg
325                    .clone()
326                    .set_raw_external_senders(&cc.session().await.crypto_provider, vec![jwk])
327                    .await
328                    .is_ok()
329            );
330        })
331        .await;
332    }
333}