Skip to main content

openmls/treesync/node/leaf_node/
capabilities.rs

1use std::collections::HashSet;
2
3use openmls_traits::{
4    crypto::OpenMlsCrypto,
5    types::{Ciphersuite, VerifiableCiphersuite},
6};
7use serde::{Deserialize, Serialize};
8use tls_codec::{TlsDeserialize, TlsDeserializeBytes, TlsSerialize, TlsSize};
9
10#[cfg(doc)]
11use super::LeafNode;
12use crate::{
13    credentials::CredentialType,
14    extensions::{
15        Extension, ExtensionType, ExtensionValidator, Extensions, RequiredCapabilitiesExtension,
16    },
17    messages::proposals::ProposalType,
18    treesync::errors::LeafNodeValidationError,
19    versions::ProtocolVersion,
20};
21
22/// Capabilities of [`LeafNode`]s.
23///
24/// ```text
25/// struct {
26///     ProtocolVersion versions<V>;
27///     CipherSuite ciphersuites<V>;
28///     ExtensionType extensions<V>;
29///     ProposalType proposals<V>;
30///     CredentialType credentials<V>;
31/// } Capabilities;
32/// ```
33#[derive(
34    Debug,
35    Clone,
36    PartialEq,
37    Eq,
38    Serialize,
39    Deserialize,
40    TlsSerialize,
41    TlsDeserialize,
42    TlsDeserializeBytes,
43    TlsSize,
44)]
45pub struct Capabilities {
46    pub(super) versions: Vec<ProtocolVersion>,
47    pub(super) ciphersuites: Vec<VerifiableCiphersuite>,
48    pub(super) extensions: Vec<ExtensionType>,
49    pub(super) proposals: Vec<ProposalType>,
50    pub(super) credentials: Vec<CredentialType>,
51}
52
53impl Capabilities {
54    /// Create a new [`Capabilities`] struct with the given configuration.
55    /// Any argument that is `None` is filled with the default values from the
56    /// global configuration.
57    // TODO(#1232)
58    pub fn new(
59        versions: Option<&[ProtocolVersion]>,
60        ciphersuites: Option<&[Ciphersuite]>,
61        extensions: Option<&[ExtensionType]>,
62        proposals: Option<&[ProposalType]>,
63        credentials: Option<&[CredentialType]>,
64    ) -> Self {
65        Self {
66            versions: match versions {
67                Some(v) => v.into(),
68                None => default_versions(),
69            },
70            ciphersuites: match ciphersuites {
71                Some(c) => c.iter().map(|c| VerifiableCiphersuite::from(*c)).collect(),
72                None => default_ciphersuites()
73                    .into_iter()
74                    .map(VerifiableCiphersuite::from)
75                    .collect(),
76            },
77            extensions: match extensions {
78                Some(e) => e.into(),
79                None => vec![],
80            },
81            proposals: match proposals {
82                Some(p) => p.into(),
83                None => vec![],
84            },
85            credentials: match credentials {
86                Some(c) => c.into(),
87                None => default_credentials(),
88            },
89        }
90    }
91
92    /// Create new empty [`Capabilities`].
93    pub fn empty() -> Self {
94        Self {
95            versions: Vec::new(),
96            ciphersuites: Vec::new(),
97            extensions: Vec::new(),
98            proposals: Vec::new(),
99            credentials: Vec::new(),
100        }
101    }
102
103    /// Creates a new [`CapabilitiesBuilder`] for constructing [`Capabilities`]
104    pub fn builder() -> CapabilitiesBuilder {
105        CapabilitiesBuilder(Self::default())
106    }
107
108    /// Creates [`Capabilities`] advertising exactly the ciphersuites supported
109    /// by the given crypto provider, with defaults for all other fields.
110    ///
111    /// In contrast to [`Capabilities::default()`], which advertises a
112    /// hardcoded ciphersuite list independently of what the crypto provider
113    /// can actually perform, this constructor derives the advertised list from
114    /// [`OpenMlsCrypto::supported_ciphersuites()`].
115    pub fn for_provider(crypto: &impl OpenMlsCrypto) -> Self {
116        Capabilities {
117            ciphersuites: crypto
118                .supported_ciphersuites()
119                .into_iter()
120                .map(VerifiableCiphersuite::from)
121                .collect(),
122            ..Default::default()
123        }
124    }
125
126    // ---------------------------------------------------------------------------------------------
127
128    /// Get a reference to the list of versions in this extension.
129    pub fn versions(&self) -> &[ProtocolVersion] {
130        &self.versions
131    }
132
133    /// Get a reference to the list of ciphersuites in this extension.
134    pub fn ciphersuites(&self) -> &[VerifiableCiphersuite] {
135        &self.ciphersuites
136    }
137
138    /// Get a reference to the list of supported extensions.
139    pub fn extensions(&self) -> &[ExtensionType] {
140        &self.extensions
141    }
142
143    /// Get a reference to the list of supported proposals.
144    pub fn proposals(&self) -> &[ProposalType] {
145        &self.proposals
146    }
147
148    /// Get a reference to the list of supported credential types.
149    pub fn credentials(&self) -> &[CredentialType] {
150        &self.credentials
151    }
152
153    // ---------------------------------------------------------------------------------------------
154
155    /// Check if these [`Capabilities`] support all the capabilities required by
156    /// the given [`RequiredCapabilitiesExtension`].
157    ///
158    /// # Errors
159    ///
160    /// Returns a [`LeafNodeValidationError`] error if any of the required
161    /// capabilities is not supported.
162    pub(crate) fn supports_required_capabilities(
163        &self,
164        required_capabilities: &RequiredCapabilitiesExtension,
165    ) -> Result<(), LeafNodeValidationError> {
166        // The required capabilities come from the wire, so all three checks use
167        // a set lookup and stop at the first unsupported entry.
168
169        // Check if all required extensions are supported.
170        let supported_extensions: HashSet<ExtensionType> =
171            self.extensions().iter().copied().collect();
172        if required_capabilities
173            .extension_types()
174            .iter()
175            .any(|e| !e.is_default() && !supported_extensions.contains(e))
176        {
177            log::error!(
178                "Leaf node does not support all required extension types\n
179                Supported extensions: {:?}\n
180                Required extensions: {:?}",
181                self.extensions(),
182                required_capabilities.extension_types()
183            );
184            return Err(LeafNodeValidationError::UnsupportedExtensions);
185        }
186        // Check if all required proposals are supported.
187        let supported_proposals: HashSet<ProposalType> = self.proposals().iter().copied().collect();
188        if required_capabilities
189            .proposal_types()
190            .iter()
191            .any(|p| !p.is_default() && !supported_proposals.contains(p))
192        {
193            return Err(LeafNodeValidationError::UnsupportedProposals);
194        }
195        // Check if all required credential types are supported.
196        let supported_credentials: HashSet<CredentialType> =
197            self.credentials().iter().copied().collect();
198        if required_capabilities
199            .credential_types()
200            .iter()
201            .any(|c| !supported_credentials.contains(c))
202        {
203            return Err(LeafNodeValidationError::UnsupportedCredentials);
204        }
205        Ok(())
206    }
207
208    /// Check if these [`Capabilities`] contain all the extensions.
209    pub(crate) fn contains_extensions(
210        &self,
211        extensions: &Extensions<impl ExtensionValidator>,
212    ) -> bool {
213        let mut required = extensions
214            .iter()
215            .map(Extension::extension_type)
216            .filter(|e| !e.is_default())
217            .peekable();
218
219        // Most leaf nodes carry no non-default extensions. Skip building the
220        // lookup set for them.
221        if required.peek().is_none() {
222            return true;
223        }
224
225        let supported: HashSet<ExtensionType> = self.extensions().iter().copied().collect();
226        required.all(|e| supported.contains(&e))
227    }
228
229    /// Check if these [`Capabilities`] contain the extension.
230    #[cfg(test)]
231    pub(crate) fn contains_extension_type(&self, extension: &ExtensionType) -> bool {
232        // Many leaf nodes carry no non-default extensions. Skip building the
233        // lookup set for them.
234        if extension.is_default() {
235            return true;
236        }
237
238        let supported: HashSet<ExtensionType> = self.extensions().iter().copied().collect();
239        supported.contains(extension)
240    }
241
242    /// Check if these [`Capabilities`] contains the credential.
243    pub(crate) fn contains_credential(&self, credential_type: CredentialType) -> bool {
244        self.credentials().contains(&credential_type)
245    }
246
247    /// Check if these [`Capabilities`] contain the version.
248    pub(crate) fn contains_version(&self, version: ProtocolVersion) -> bool {
249        self.versions().contains(&version)
250    }
251
252    /// Check if these [`Capabilities`] contain the ciphersuite.
253    pub(crate) fn contains_ciphersuite(&self, ciphersuite: VerifiableCiphersuite) -> bool {
254        self.ciphersuites().contains(&ciphersuite)
255    }
256
257    /// Add random GREASE values to the capabilities to ensure extensibility.
258    ///
259    /// This adds one random GREASE value to each capability list if no GREASE
260    /// value is already present:
261    /// - Ciphersuites
262    /// - Extensions
263    /// - Proposals
264    /// - Credentials
265    ///
266    /// GREASE values are used per [RFC 9420 Section 13.5](https://www.rfc-editor.org/rfc/rfc9420.html#section-13.5)
267    /// to help prevent extensibility failures by ensuring implementations properly
268    /// handle unknown values.
269    ///
270    /// # Example
271    ///
272    /// ```
273    /// use openmls::prelude::*;
274    /// use openmls_rust_crypto::OpenMlsRustCrypto;
275    ///
276    /// let provider = OpenMlsRustCrypto::default();
277    ///
278    /// // Create capabilities with GREASE values injected
279    /// let capabilities = Capabilities::builder()
280    ///     .build()
281    ///     .with_grease(provider.rand());
282    ///
283    /// // Verify GREASE values were added
284    /// assert!(capabilities.ciphersuites().iter().any(|cs| cs.is_grease()));
285    /// assert!(capabilities.extensions().iter().any(|ext| ext.is_grease()));
286    /// assert!(capabilities.proposals().iter().any(|prop| prop.is_grease()));
287    /// assert!(capabilities.credentials().iter().any(|cred| cred.is_grease()));
288    /// ```
289    pub fn with_grease(mut self, rand: &impl openmls_traits::random::OpenMlsRand) -> Self {
290        use crate::credentials::CredentialType;
291        use crate::extensions::ExtensionType;
292        use crate::messages::proposals::ProposalType;
293        use openmls_traits::types::VerifiableCiphersuite;
294
295        // Add GREASE ciphersuite if none present
296        if !self.ciphersuites.iter().any(|cs| cs.is_grease()) {
297            let grease_cs = VerifiableCiphersuite::new(crate::grease::random_grease_value(rand));
298            self.ciphersuites.push(grease_cs);
299        }
300
301        // Add GREASE extension if none present
302        if !self.extensions.iter().any(|ext| ext.is_grease()) {
303            let grease_ext = ExtensionType::Grease(crate::grease::random_grease_value(rand));
304            self.extensions.push(grease_ext);
305        }
306
307        // Add GREASE proposal if none present
308        if !self.proposals.iter().any(|prop| prop.is_grease()) {
309            let grease_prop = ProposalType::Grease(crate::grease::random_grease_value(rand));
310            self.proposals.push(grease_prop);
311        }
312
313        // Add GREASE credential if none present
314        if !self.credentials.iter().any(|cred| cred.is_grease()) {
315            let grease_cred = CredentialType::Grease(crate::grease::random_grease_value(rand));
316            self.credentials.push(grease_cred);
317        }
318
319        self
320    }
321}
322
323/// A helper for building [`Capabilities`]
324#[derive(Debug, Clone)]
325pub struct CapabilitiesBuilder(Capabilities);
326
327impl CapabilitiesBuilder {
328    /// Sets the `versions` field on the [`Capabilities`].
329    pub fn versions(self, versions: Vec<ProtocolVersion>) -> Self {
330        Self(Capabilities { versions, ..self.0 })
331    }
332
333    /// Sets the `ciphersuites` field on the [`Capabilities`].
334    pub fn ciphersuites(self, ciphersuites: Vec<Ciphersuite>) -> Self {
335        let ciphersuites = ciphersuites.into_iter().map(|cs| cs.into()).collect();
336
337        Self(Capabilities {
338            ciphersuites,
339            ..self.0
340        })
341    }
342
343    /// Sets the `extensions` field on the [`Capabilities`].
344    pub fn extensions(self, extensions: Vec<ExtensionType>) -> Self {
345        Self(Capabilities {
346            extensions,
347            ..self.0
348        })
349    }
350
351    /// Sets the `proposals` field on the [`Capabilities`].
352    pub fn proposals(self, proposals: Vec<ProposalType>) -> Self {
353        Self(Capabilities {
354            proposals,
355            ..self.0
356        })
357    }
358
359    /// Sets the `credentials` field on the [`Capabilities`].
360    pub fn credentials(self, credentials: Vec<CredentialType>) -> Self {
361        Self(Capabilities {
362            credentials,
363            ..self.0
364        })
365    }
366
367    /// Adds random GREASE values to the capabilities being built.
368    ///
369    /// This is a convenience method that calls [`Capabilities::with_grease`] on the
370    /// built capabilities. See that method for more details.
371    ///
372    /// # Example
373    ///
374    /// ```
375    /// use openmls::prelude::*;
376    /// use openmls_rust_crypto::OpenMlsRustCrypto;
377    ///
378    /// let provider = OpenMlsRustCrypto::default();
379    ///
380    /// let capabilities = Capabilities::builder()
381    ///     .with_grease(provider.rand())
382    ///     .build();
383    ///
384    /// // GREASE values were added
385    /// assert!(capabilities.ciphersuites().iter().any(|cs| cs.is_grease()));
386    /// ```
387    pub fn with_grease(self, rand: &impl openmls_traits::random::OpenMlsRand) -> Self {
388        Self(self.0.with_grease(rand))
389    }
390
391    /// Builds the [`Capabilities`].
392    pub fn build(self) -> Capabilities {
393        self.0
394    }
395}
396
397#[cfg(test)]
398impl Capabilities {
399    /// Set the versions list.
400    pub fn set_versions(&mut self, versions: Vec<ProtocolVersion>) {
401        self.versions = versions;
402    }
403
404    /// Set the ciphersuites list.
405    pub fn set_ciphersuites(&mut self, ciphersuites: Vec<VerifiableCiphersuite>) {
406        self.ciphersuites = ciphersuites;
407    }
408}
409
410impl Default for Capabilities {
411    fn default() -> Self {
412        Capabilities {
413            versions: default_versions(),
414            ciphersuites: default_ciphersuites()
415                .into_iter()
416                .map(VerifiableCiphersuite::from)
417                .collect(),
418            extensions: vec![],
419            proposals: vec![],
420            credentials: default_credentials(),
421        }
422    }
423}
424
425pub(super) fn default_versions() -> Vec<ProtocolVersion> {
426    vec![ProtocolVersion::Mls10]
427}
428
429pub(super) fn default_ciphersuites() -> Vec<Ciphersuite> {
430    vec![
431        Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519,
432        Ciphersuite::MLS_128_DHKEMP256_AES128GCM_SHA256_P256,
433        Ciphersuite::MLS_128_DHKEMX25519_CHACHA20POLY1305_SHA256_Ed25519,
434        #[cfg(feature = "draft-ietf-mls-pq-ciphersuites")]
435        Ciphersuite::MLS_256_XWING_CHACHA20POLY1305_SHA256_Ed25519,
436        #[cfg(feature = "draft-ietf-mls-pq-ciphersuites")]
437        Ciphersuite::MLS_192_MLKEM1024_AES256GCM_SHA384_P384,
438        #[cfg(feature = "draft-ietf-mls-pq-ciphersuites")]
439        Ciphersuite::MLS_256_MLKEM1024_AES256GCM_SHA512_MLDSA87,
440        #[cfg(feature = "draft-ietf-mls-pq-ciphersuites")]
441        Ciphersuite::MLS_128_MLKEM768X25519_AES256GCM_SHA384_Ed25519,
442        #[cfg(feature = "draft-ietf-mls-pq-ciphersuites")]
443        Ciphersuite::MLS_128_MLKEM768X25519_AES128GCM_SHA256_Ed25519,
444        #[cfg(feature = "draft-ietf-mls-pq-ciphersuites")]
445        Ciphersuite::MLS_128_MLKEM768_AES256GCM_SHA384_P256,
446        #[cfg(feature = "draft-ietf-mls-pq-ciphersuites")]
447        Ciphersuite::MLS_128_MLKEM768X25519_CHACHA20POLY1305_SHA384_MLDSA44,
448        #[cfg(feature = "draft-ietf-mls-pq-ciphersuites")]
449        Ciphersuite::MLS_192_MLKEM768_AES256GCM_SHA384_MLDSA65,
450        #[cfg(feature = "draft-ietf-mls-pq-ciphersuites")]
451        Ciphersuite::MLS_256_MLKEM1024_AES256GCM_SHA384_MLDSA87,
452        #[cfg(feature = "draft-ietf-mls-pq-ciphersuites")]
453        Ciphersuite::MLS_128_MLKEM768_AES256GCM_SHA384_Ed25519,
454    ]
455}
456
457// TODO(#1231)
458pub(super) fn default_credentials() -> Vec<CredentialType> {
459    vec![CredentialType::Basic]
460}
461
462#[cfg(test)]
463mod tests {
464    use openmls_traits::{
465        crypto::OpenMlsCrypto,
466        types::{Ciphersuite, VerifiableCiphersuite},
467    };
468    use tls_codec::{Deserialize, Serialize};
469
470    use super::Capabilities;
471    use crate::{
472        credentials::CredentialType, messages::proposals::ProposalType, prelude::ExtensionType,
473        versions::ProtocolVersion,
474    };
475
476    #[test]
477    fn that_unknown_capabilities_are_de_serialized_correctly() {
478        let versions = vec![ProtocolVersion::Mls10, ProtocolVersion::Other(999)];
479        let ciphersuites = vec![
480            Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519.into(),
481            Ciphersuite::MLS_128_DHKEMP256_AES128GCM_SHA256_P256.into(),
482            Ciphersuite::MLS_128_DHKEMX25519_CHACHA20POLY1305_SHA256_Ed25519.into(),
483            Ciphersuite::MLS_256_DHKEMX448_AES256GCM_SHA512_Ed448.into(),
484            Ciphersuite::MLS_256_DHKEMP521_AES256GCM_SHA512_P521.into(),
485            Ciphersuite::MLS_256_DHKEMX448_CHACHA20POLY1305_SHA512_Ed448.into(),
486            Ciphersuite::MLS_256_DHKEMP384_AES256GCM_SHA384_P384.into(),
487            VerifiableCiphersuite::new(0x0000),
488            // Use non-GREASE values (GREASE pattern is 0x_A_A)
489            VerifiableCiphersuite::new(0x0B0B),
490            VerifiableCiphersuite::new(0x7C7C),
491            VerifiableCiphersuite::new(0xF000),
492            VerifiableCiphersuite::new(0xFFFF),
493        ];
494
495        let extensions = vec![
496            ExtensionType::Unknown(0x0000),
497            ExtensionType::Unknown(0xFAFA),
498        ];
499
500        // Use non-GREASE values
501        let proposals = vec![ProposalType::Custom(0x7C7C)];
502
503        let credentials = vec![
504            CredentialType::Basic,
505            CredentialType::X509,
506            CredentialType::Other(0x0000),
507            // Use non-GREASE values
508            CredentialType::Other(0x7C7C),
509            CredentialType::Other(0xFFFF),
510        ];
511
512        let expected = Capabilities {
513            versions,
514            ciphersuites,
515            extensions,
516            proposals,
517            credentials,
518        };
519
520        let test_serialized = expected.tls_serialize_detached().unwrap();
521
522        let got = Capabilities::tls_deserialize_exact(test_serialized).unwrap();
523
524        assert_eq!(expected, got);
525    }
526
527    #[test]
528    fn for_provider_advertises_exactly_the_supported_ciphersuites() {
529        let crypto = openmls_rust_crypto::RustCrypto::default();
530        let capabilities = Capabilities::for_provider(&crypto);
531
532        let expected: Vec<VerifiableCiphersuite> = crypto
533            .supported_ciphersuites()
534            .into_iter()
535            .map(VerifiableCiphersuite::from)
536            .collect();
537        assert_eq!(capabilities.ciphersuites(), expected.as_slice());
538    }
539}