Skip to main content

core_crypto/mls/conversation/
config.rs

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