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