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