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