core_crypto/mls/conversation/
config.rs1use 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
16pub(crate) const MAX_PAST_EPOCHS: usize = 3;
18
19pub(crate) const OUT_OF_ORDER_TOLERANCE: u32 = 2;
22
23pub(crate) const MAXIMUM_FORWARD_DISTANCE: u32 = 1000;
25
26#[derive(Debug, Clone, Default)]
28pub struct ConversationConfiguration {
29 pub cipher_suite: CipherSuite,
31 pub external_senders: Vec<ExternalSender>,
33 pub custom: CustomConfiguration,
35}
36
37impl ConversationConfiguration {
38 pub(crate) const PADDING_SIZE: usize = 128;
40
41 pub(crate) const DEFAULT_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::Mls10;
43
44 pub(crate) const DEFAULT_SUPPORTED_CREDENTIALS: &'static [CredentialType] =
46 &[CredentialType::Basic, CredentialType::X509];
47
48 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 ];
56
57 const NUMBER_RESUMPTION_PSK: usize = 1;
59
60 #[inline(always)]
62 pub fn as_openmls_default_configuration(&self) -> Result<openmls::group::MlsGroupConfig> {
63 let crypto_config = openmls::prelude::CryptoConfig {
64 version: Self::DEFAULT_PROTOCOL_VERSION,
65 ciphersuite: self.cipher_suite.into(),
66 };
67 Ok(openmls::group::MlsGroupConfig::builder()
68 .wire_format_policy(self.custom.wire_policy.into())
69 .max_past_epochs(MAX_PAST_EPOCHS)
70 .padding_size(Self::PADDING_SIZE)
71 .number_of_resumption_psks(Self::NUMBER_RESUMPTION_PSK)
72 .leaf_capabilities(Self::default_leaf_capabilities())
73 .required_capabilities(self.default_required_capabilities())
74 .sender_ratchet_configuration(SenderRatchetConfiguration::new(
75 self.custom.out_of_order_tolerance,
76 self.custom.maximum_forward_distance,
77 ))
78 .use_ratchet_tree_extension(true)
79 .external_senders(self.external_senders.iter().cloned().map(Into::into).collect())
80 .crypto_config(crypto_config)
81 .build())
82 }
83
84 pub fn default_leaf_capabilities() -> Capabilities {
86 Capabilities::new(
87 Some(&[Self::DEFAULT_PROTOCOL_VERSION]),
88 Some(Self::DEFAULT_SUPPORTED_CIPHERSUITES),
89 Some(&[]),
90 Some(&[]),
91 Some(Self::DEFAULT_SUPPORTED_CREDENTIALS),
92 )
93 }
94
95 fn default_required_capabilities(&self) -> RequiredCapabilitiesExtension {
96 RequiredCapabilitiesExtension::new(&[], &[], Self::DEFAULT_SUPPORTED_CREDENTIALS)
97 }
98
99 pub async fn set_external_senders(
102 &mut self,
103 external_senders: impl IntoIterator<Item = ExternalSender>,
104 ) -> Result<()> {
105 self.external_senders = external_senders.into_iter().collect();
106 Ok(())
107 }
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct CustomConfiguration {
113 pub key_rotation_span: Option<std::time::Duration>,
116 pub wire_policy: WirePolicy,
118 pub out_of_order_tolerance: u32,
122 pub maximum_forward_distance: u32,
125}
126
127impl Default for CustomConfiguration {
128 fn default() -> Self {
129 Self {
130 wire_policy: WirePolicy::Plaintext,
131 key_rotation_span: Default::default(),
132 out_of_order_tolerance: OUT_OF_ORDER_TOLERANCE,
133 maximum_forward_distance: MAXIMUM_FORWARD_DISTANCE,
134 }
135 }
136}
137
138#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Serialize, Deserialize)]
140#[repr(u8)]
141pub enum WirePolicy {
142 #[default]
144 Plaintext = 1,
145 Ciphertext = 2,
147}
148
149impl From<WirePolicy> for WireFormatPolicy {
150 fn from(policy: WirePolicy) -> Self {
151 match policy {
152 WirePolicy::Ciphertext => PURE_CIPHERTEXT_WIRE_FORMAT_POLICY,
153 WirePolicy::Plaintext => PURE_PLAINTEXT_WIRE_FORMAT_POLICY,
154 }
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use openmls::prelude::ProtocolVersion;
161 use openmls_traits::{
162 OpenMlsCryptoProvider,
163 crypto::OpenMlsCrypto,
164 types::{SignatureScheme, VerifiableCiphersuite},
165 };
166 use wire_e2e_identity::JwsAlgorithm;
167
168 use crate::{ConversationConfiguration, ExternalSender, test_utils::*};
169
170 #[macro_rules_attribute::apply(smol_macros::test)]
171 async fn group_should_have_required_capabilities() {
172 let case = TestContext::default();
173
174 let [session] = case.sessions().await;
175 Box::pin(async move {
176 let conversation = case.create_conversation([&session]).await;
177 let guard = conversation.guard().await;
178 let group = guard.group().await;
179
180 let capabilities = group.group_context_extensions().required_capabilities().unwrap();
181
182 assert!(capabilities.extension_types().is_empty());
184 assert!(capabilities.proposal_types().is_empty());
185 assert_eq!(
186 capabilities.credential_types(),
187 ConversationConfiguration::DEFAULT_SUPPORTED_CREDENTIALS
188 );
189 })
190 .await
191 }
192
193 #[apply(all_cred_cipher)]
194 pub async fn creator_leaf_node_should_have_default_capabilities(case: TestContext) {
195 let [session] = case.sessions().await;
196 Box::pin(async move {
197 let conversation = case.create_conversation([&session]).await;
198 let guard = conversation.guard().await;
199 let group = guard.group().await;
200
201 let creator_capabilities = group.own_leaf().unwrap().capabilities();
203
204 assert_eq!(creator_capabilities.versions(), &[ProtocolVersion::Mls10]);
207
208 assert_eq!(
210 creator_capabilities.ciphersuites().to_vec(),
211 ConversationConfiguration::DEFAULT_SUPPORTED_CIPHERSUITES
212 .iter()
213 .map(|c| VerifiableCiphersuite::from(*c))
214 .collect::<Vec<_>>()
215 );
216
217 assert!(creator_capabilities.proposals().is_empty());
219
220 assert!(creator_capabilities.extensions().is_empty(),);
222
223 assert_eq!(
225 creator_capabilities.credentials(),
226 ConversationConfiguration::DEFAULT_SUPPORTED_CREDENTIALS
227 );
228 })
229 .await
230 }
231
232 #[apply(all_cred_cipher)]
233 pub async fn should_support_raw_external_sender(case: TestContext) {
234 let [cc] = case.sessions().await;
235 Box::pin(async move {
236 let (_sk, pk) = cc
237 .transaction
238 .crypto_provider()
239 .await
240 .unwrap()
241 .crypto()
242 .signature_key_gen(case.signature_scheme())
243 .unwrap();
244 let pk = ExternalSender::parse_public_key(&pk, case.signature_scheme()).unwrap();
245
246 assert!(case.cfg.clone().set_external_senders([pk]).await.is_ok());
247 })
248 .await
249 }
250
251 #[apply(all_cred_cipher)]
252 pub async fn should_support_jwk_external_sender(case: TestContext) {
253 Box::pin(async move {
254 let sc = case.signature_scheme();
255
256 let alg = match sc {
257 SignatureScheme::ED25519 => JwsAlgorithm::Ed25519,
258 SignatureScheme::ECDSA_SECP256R1_SHA256 => JwsAlgorithm::P256,
259 SignatureScheme::ECDSA_SECP384R1_SHA384 => JwsAlgorithm::P384,
260 SignatureScheme::ECDSA_SECP521R1_SHA512 => JwsAlgorithm::P521,
261 SignatureScheme::ED448 => unreachable!(),
262 };
263
264 let jwk = wire_e2e_identity::generate_jwk(alg);
265 let external_sender = ExternalSender::parse_jwk(&jwk).unwrap();
266 assert!(case.cfg.clone().set_external_senders([external_sender]).await.is_ok());
267 })
268 .await;
269 }
270}