core_crypto/mls/conversation/
config.rs1use mls_crypto_provider::MlsCryptoProvider;
23use openmls::prelude::{
24 Capabilities, Credential, CredentialType, ExternalSender, OpenMlsSignaturePublicKey,
25 PURE_CIPHERTEXT_WIRE_FORMAT_POLICY, PURE_PLAINTEXT_WIRE_FORMAT_POLICY, ProtocolVersion,
26 RequiredCapabilitiesExtension, SenderRatchetConfiguration, WireFormatPolicy,
27};
28use openmls_traits::{
29 OpenMlsCryptoProvider,
30 crypto::OpenMlsCrypto,
31 types::{Ciphersuite, SignatureScheme},
32};
33use serde::{Deserialize, Serialize};
34use wire_e2e_identity::prelude::parse_json_jwk;
35
36use super::Result;
37use crate::{MlsError, RecursiveError, context::CentralContext, prelude::MlsCiphersuite};
38
39pub(crate) const MAX_PAST_EPOCHS: usize = 3;
41
42pub(crate) const OUT_OF_ORDER_TOLERANCE: u32 = 2;
45
46pub(crate) const MAXIMUM_FORWARD_DISTANCE: u32 = 1000;
48
49impl CentralContext {
50 pub async fn set_raw_external_senders(
52 &self,
53 cfg: &mut MlsConversationConfiguration,
54 external_senders: Vec<Vec<u8>>,
55 ) -> Result<()> {
56 let mls_provider = self
57 .mls_provider()
58 .await
59 .map_err(RecursiveError::root("getting mls provider"))?;
60 cfg.external_senders = external_senders
61 .into_iter()
62 .map(|key| {
63 MlsConversationConfiguration::parse_external_sender(&key).or_else(|_| {
64 MlsConversationConfiguration::legacy_external_sender(
65 key,
66 cfg.ciphersuite.signature_algorithm(),
67 &mls_provider,
68 )
69 })
70 })
71 .collect::<Result<_>>()?;
72 Ok(())
73 }
74}
75
76#[derive(Debug, Clone, Default)]
78pub struct MlsConversationConfiguration {
79 pub ciphersuite: MlsCiphersuite,
81 pub external_senders: Vec<ExternalSender>,
83 pub custom: MlsCustomConfiguration,
85}
86
87impl MlsConversationConfiguration {
88 const WIRE_SERVER_IDENTITY: &'static str = "wire-server";
89
90 const PADDING_SIZE: usize = 128;
91
92 pub(crate) const DEFAULT_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::Mls10;
94
95 pub(crate) const DEFAULT_SUPPORTED_CREDENTIALS: &'static [CredentialType] =
97 &[CredentialType::Basic, CredentialType::X509];
98
99 pub(crate) const DEFAULT_SUPPORTED_CIPHERSUITES: &'static [Ciphersuite] = &[
101 Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519,
102 Ciphersuite::MLS_128_DHKEMP256_AES128GCM_SHA256_P256,
103 Ciphersuite::MLS_128_DHKEMX25519_CHACHA20POLY1305_SHA256_Ed25519,
104 Ciphersuite::MLS_256_DHKEMP384_AES256GCM_SHA384_P384,
105 Ciphersuite::MLS_256_DHKEMP521_AES256GCM_SHA512_P521,
106 ];
107
108 const NUMBER_RESUMPTION_PSK: usize = 1;
110
111 #[inline(always)]
113 pub fn as_openmls_default_configuration(&self) -> Result<openmls::group::MlsGroupConfig> {
114 let crypto_config = openmls::prelude::CryptoConfig {
115 version: Self::DEFAULT_PROTOCOL_VERSION,
116 ciphersuite: self.ciphersuite.into(),
117 };
118 Ok(openmls::group::MlsGroupConfig::builder()
119 .wire_format_policy(self.custom.wire_policy.into())
120 .max_past_epochs(MAX_PAST_EPOCHS)
121 .padding_size(Self::PADDING_SIZE)
122 .number_of_resumption_psks(Self::NUMBER_RESUMPTION_PSK)
123 .leaf_capabilities(Self::default_leaf_capabilities())
124 .required_capabilities(self.default_required_capabilities())
125 .sender_ratchet_configuration(SenderRatchetConfiguration::new(
126 self.custom.out_of_order_tolerance,
127 self.custom.maximum_forward_distance,
128 ))
129 .use_ratchet_tree_extension(true)
130 .external_senders(self.external_senders.clone())
131 .crypto_config(crypto_config)
132 .build())
133 }
134
135 pub fn default_leaf_capabilities() -> Capabilities {
137 Capabilities::new(
138 Some(&[Self::DEFAULT_PROTOCOL_VERSION]),
139 Some(Self::DEFAULT_SUPPORTED_CIPHERSUITES),
140 Some(&[]),
141 Some(&[]),
142 Some(Self::DEFAULT_SUPPORTED_CREDENTIALS),
143 )
144 }
145
146 fn default_required_capabilities(&self) -> RequiredCapabilitiesExtension {
147 RequiredCapabilitiesExtension::new(&[], &[], Self::DEFAULT_SUPPORTED_CREDENTIALS)
148 }
149
150 fn parse_external_sender(jwk: &[u8]) -> Result<ExternalSender> {
152 let pk = parse_json_jwk(jwk)
153 .map_err(wire_e2e_identity::prelude::E2eIdentityError::from)
154 .map_err(crate::e2e_identity::Error::from)
155 .map_err(RecursiveError::e2e_identity("parsing jwk"))?;
156 Ok(ExternalSender::new(
157 pk.into(),
158 Credential::new_basic(Self::WIRE_SERVER_IDENTITY.into()),
159 ))
160 }
161
162 fn legacy_external_sender(
166 key: Vec<u8>,
167 signature_scheme: SignatureScheme,
168 backend: &MlsCryptoProvider,
169 ) -> Result<ExternalSender> {
170 backend
171 .crypto()
172 .validate_signature_key(signature_scheme, &key[..])
173 .map_err(MlsError::wrap("validating signature key"))?;
174 let key = OpenMlsSignaturePublicKey::new(key.into(), signature_scheme)
175 .map_err(MlsError::wrap("creating new signature public key"))?;
176 Ok(ExternalSender::new(
177 key.into(),
178 Credential::new_basic(Self::WIRE_SERVER_IDENTITY.into()),
179 ))
180 }
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct MlsCustomConfiguration {
186 pub key_rotation_span: Option<std::time::Duration>,
189 pub wire_policy: MlsWirePolicy,
191 pub out_of_order_tolerance: u32,
195 pub maximum_forward_distance: u32,
198}
199
200impl Default for MlsCustomConfiguration {
201 fn default() -> Self {
202 Self {
203 wire_policy: MlsWirePolicy::Plaintext,
204 key_rotation_span: Default::default(),
205 out_of_order_tolerance: OUT_OF_ORDER_TOLERANCE,
206 maximum_forward_distance: MAXIMUM_FORWARD_DISTANCE,
207 }
208 }
209}
210
211#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Serialize, Deserialize)]
213#[repr(u8)]
214pub enum MlsWirePolicy {
215 #[default]
217 Plaintext = 1,
218 Ciphertext = 2,
220}
221
222impl From<MlsWirePolicy> for WireFormatPolicy {
223 fn from(policy: MlsWirePolicy) -> Self {
224 match policy {
225 MlsWirePolicy::Ciphertext => PURE_CIPHERTEXT_WIRE_FORMAT_POLICY,
226 MlsWirePolicy::Plaintext => PURE_PLAINTEXT_WIRE_FORMAT_POLICY,
227 }
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use openmls::prelude::ProtocolVersion;
234 use openmls_traits::{
235 OpenMlsCryptoProvider,
236 crypto::OpenMlsCrypto,
237 types::{SignatureScheme, VerifiableCiphersuite},
238 };
239 use wasm_bindgen_test::*;
240 use wire_e2e_identity::prelude::JwsAlgorithm;
241
242 use crate::mls::conversation::ConversationWithMls as _;
243 use crate::{prelude::MlsConversationConfiguration, test_utils::*};
244
245 wasm_bindgen_test_configure!(run_in_browser);
246
247 #[cfg_attr(not(target_family = "wasm"), async_std::test)]
248 #[wasm_bindgen_test]
249 pub async fn group_should_have_required_capabilities() {
250 let case = TestCase::default();
251 run_test_with_client_ids(case.clone(), ["alice"], move |[cc]| {
252 Box::pin(async move {
253 let id = conversation_id();
254 cc.context
255 .new_conversation(&id, case.credential_type, case.cfg.clone())
256 .await
257 .unwrap();
258 let conv = cc.context.conversation(&id).await.unwrap();
259 let group = conv.conversation().await;
260
261 let capabilities = group.group.group_context_extensions().required_capabilities().unwrap();
262
263 assert!(capabilities.extension_types().is_empty());
265 assert!(capabilities.proposal_types().is_empty());
266 assert_eq!(
267 capabilities.credential_types(),
268 MlsConversationConfiguration::DEFAULT_SUPPORTED_CREDENTIALS
269 );
270 })
271 })
272 .await
273 }
274
275 #[apply(all_cred_cipher)]
276 #[wasm_bindgen_test]
277 pub async fn creator_leaf_node_should_have_default_capabilities(case: TestCase) {
278 run_test_with_client_ids(case.clone(), ["alice"], move |[cc]| {
279 Box::pin(async move {
280 let id = conversation_id();
281 cc.context
282 .new_conversation(&id, case.credential_type, case.cfg.clone())
283 .await
284 .unwrap();
285 let conv = cc.context.conversation(&id).await.unwrap();
286 let group = conv.conversation().await;
287
288 let creator_capabilities = group.group.own_leaf().unwrap().capabilities();
290
291 assert_eq!(creator_capabilities.versions(), &[ProtocolVersion::Mls10]);
294
295 assert_eq!(
297 creator_capabilities.ciphersuites().to_vec(),
298 MlsConversationConfiguration::DEFAULT_SUPPORTED_CIPHERSUITES
299 .iter()
300 .map(|c| VerifiableCiphersuite::from(*c))
301 .collect::<Vec<_>>()
302 );
303
304 assert!(creator_capabilities.proposals().is_empty());
306
307 assert!(creator_capabilities.extensions().is_empty(),);
309
310 assert_eq!(
312 creator_capabilities.credentials(),
313 MlsConversationConfiguration::DEFAULT_SUPPORTED_CREDENTIALS
314 );
315 })
316 })
317 .await
318 }
319
320 #[apply(all_cred_cipher)]
321 #[wasm_bindgen_test]
322 pub async fn should_support_raw_external_sender(case: TestCase) {
323 run_test_with_client_ids(case.clone(), ["alice"], move |[cc]| {
324 Box::pin(async move {
325 let (_sk, pk) = cc
326 .context
327 .mls_provider()
328 .await
329 .unwrap()
330 .crypto()
331 .signature_key_gen(case.signature_scheme())
332 .unwrap();
333
334 assert!(
335 cc.context
336 .set_raw_external_senders(&mut case.cfg.clone(), vec![pk])
337 .await
338 .is_ok()
339 );
340 })
341 })
342 .await
343 }
344
345 #[apply(all_cred_cipher)]
346 #[wasm_bindgen_test]
347 pub async fn should_support_jwk_external_sender(case: TestCase) {
348 run_test_with_client_ids(case.clone(), ["alice"], move |[cc]| {
349 Box::pin(async move {
350 let sc = case.signature_scheme();
351
352 let alg = match sc {
353 SignatureScheme::ED25519 => JwsAlgorithm::Ed25519,
354 SignatureScheme::ECDSA_SECP256R1_SHA256 => JwsAlgorithm::P256,
355 SignatureScheme::ECDSA_SECP384R1_SHA384 => JwsAlgorithm::P384,
356 SignatureScheme::ECDSA_SECP521R1_SHA512 => JwsAlgorithm::P521,
357 SignatureScheme::ED448 => unreachable!(),
358 };
359
360 let jwk = wire_e2e_identity::prelude::generate_jwk(alg);
361 cc.context
362 .set_raw_external_senders(&mut case.cfg.clone(), vec![jwk])
363 .await
364 .unwrap();
365 })
366 })
367 .await;
368 }
369}