Skip to main content

openmls/treesync/node/leaf_node/
capabilities.rs

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