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
10use super::LeafNode;
11use crate::{
12    credentials::CredentialType,
13    extensions::{
14        Extension, ExtensionType, ExtensionValidator, Extensions, RequiredCapabilitiesExtension,
15    },
16    key_packages::KeyPackage,
17    messages::proposals::ProposalType,
18    treesync::errors::LeafNodeValidationError,
19    versions::ProtocolVersion,
20};
21
22/// How a leaf's capabilities are treated when they don't already cover what
23/// the leaf being built needs.
24///
25/// There is deliberately no [`Default`]: which policy applies depends on
26/// whether the caller set capabilities at all, so every call site resolves it
27/// through `resolve_capabilities` rather than falling back on its own.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
29pub enum CapabilitiesPolicy {
30    /// Reject construction unless the given capabilities already cover the
31    /// leaf's own ciphersuite, credential type and extension types, and the
32    /// group's required capabilities.
33    Reject,
34    /// Add whatever the leaf itself uses — its ciphersuite, credential type
35    /// and extension types, plus the extension types of the KeyPackage the
36    /// leaf is built for — and reject only if the group's required
37    /// capabilities are still unmet. Never removes anything the caller set.
38    ///
39    /// Note that this widens only capabilities the library can derive from the
40    /// leaf. It never claims support for the group's required extension,
41    /// proposal or credential types on its own, and it cannot
42    /// cover the cross-member credential checks a group applies on receipt.
43    /// Therefore, a widened leaf is not automatically an acceptable one.
44    Widen,
45}
46
47/// Resolve builder-level capabilities and policy into what leaf construction
48/// needs.
49///
50/// Capabilities the caller never set are widened from the leaf itself: there is
51/// exactly one sensible answer for what an unconfigured leaf should advertise,
52/// and it is derivable. Capabilities the caller set explicitly are held to
53/// exactly what they listed, because a list that doesn't cover the leaf is a
54/// mistake worth reporting rather than papering over.
55///
56/// An explicit policy always wins over both defaults.
57pub(crate) fn resolve_capabilities(
58    capabilities: Option<Capabilities>,
59    policy: Option<CapabilitiesPolicy>,
60) -> (Capabilities, CapabilitiesPolicy) {
61    match capabilities {
62        Some(capabilities) => (capabilities, policy.unwrap_or(CapabilitiesPolicy::Reject)),
63        None => (
64            Capabilities::default(),
65            policy.unwrap_or(CapabilitiesPolicy::Widen),
66        ),
67    }
68}
69
70/// Like [`resolve_capabilities`], but for a leaf that already exists in a tree.
71///
72/// Unset capabilities are inherited from that leaf rather than derived from
73/// scratch: they were already validated when it entered the tree, so there is
74/// nothing to widen from, and a rejection means the group's requirements have
75/// changed since.
76pub(crate) fn resolve_capabilities_for_existing_leaf(
77    capabilities: Option<Capabilities>,
78    policy: Option<CapabilitiesPolicy>,
79    existing: &Capabilities,
80) -> (Capabilities, CapabilitiesPolicy) {
81    match capabilities {
82        Some(_) => resolve_capabilities(capabilities, policy),
83        None => (
84            existing.clone(),
85            policy.unwrap_or(CapabilitiesPolicy::Reject),
86        ),
87    }
88}
89
90/// Capabilities of [`LeafNode`]s.
91///
92/// ```text
93/// struct {
94///     ProtocolVersion versions<V>;
95///     CipherSuite ciphersuites<V>;
96///     ExtensionType extensions<V>;
97///     ProposalType proposals<V>;
98///     CredentialType credentials<V>;
99/// } Capabilities;
100/// ```
101#[derive(
102    Debug,
103    Clone,
104    PartialEq,
105    Eq,
106    Serialize,
107    Deserialize,
108    TlsSerialize,
109    TlsDeserialize,
110    TlsDeserializeBytes,
111    TlsSize,
112)]
113pub struct Capabilities {
114    pub(super) versions: Vec<ProtocolVersion>,
115    pub(super) ciphersuites: Vec<VerifiableCiphersuite>,
116    pub(super) extensions: Vec<ExtensionType>,
117    pub(super) proposals: Vec<ProposalType>,
118    pub(super) credentials: Vec<CredentialType>,
119}
120
121impl Capabilities {
122    /// Create a new [`Capabilities`] struct with the given configuration.
123    ///
124    /// Only `versions` has a default (`Mls1.0`); every other `None`
125    /// produces an **empty** list. An empty list is an
126    /// explicit statement that nothing is supported, so a leaf built from it is
127    /// rejected unless the caller also asks for [`CapabilitiesPolicy::Widen`].
128    ///
129    /// Prefer [`Capabilities::builder`], which makes the empty starting point
130    /// obvious.
131    // TODO(#1232)
132    pub fn new(
133        versions: Option<&[ProtocolVersion]>,
134        ciphersuites: Option<&[Ciphersuite]>,
135        extensions: Option<&[ExtensionType]>,
136        proposals: Option<&[ProposalType]>,
137        credentials: Option<&[CredentialType]>,
138    ) -> Self {
139        Self {
140            versions: match versions {
141                Some(v) => v.into(),
142                None => default_versions(),
143            },
144            ciphersuites: match ciphersuites {
145                Some(c) => c.iter().map(|c| VerifiableCiphersuite::from(*c)).collect(),
146                None => vec![],
147            },
148            extensions: match extensions {
149                Some(e) => e.into(),
150                None => vec![],
151            },
152            proposals: match proposals {
153                Some(p) => p.into(),
154                None => vec![],
155            },
156            credentials: match credentials {
157                Some(c) => c.into(),
158                None => vec![],
159            },
160        }
161    }
162
163    /// Create new empty [`Capabilities`].
164    pub fn empty() -> Self {
165        Self {
166            versions: Vec::new(),
167            ciphersuites: Vec::new(),
168            extensions: Vec::new(),
169            proposals: Vec::new(),
170            credentials: Vec::new(),
171        }
172    }
173
174    /// Creates a new [`CapabilitiesBuilder`] for constructing [`Capabilities`].
175    ///
176    /// Starts from empty lists, except `versions`, which is seeded with
177    /// `default_versions` since the library currently only supports `Mls10`.
178    pub fn builder() -> CapabilitiesBuilder {
179        CapabilitiesBuilder(Self::default())
180    }
181
182    /// Creates [`Capabilities`] advertising exactly the ciphersuites supported
183    /// by the given crypto provider, derived from
184    /// [`OpenMlsCrypto::supported_ciphersuites()`].
185    ///
186    /// All other lists are left empty, as everywhere else; only `versions` is
187    /// seeded. Useful for a client that wants to advertise its full crypto
188    /// reach rather than just the one ciphersuite a given leaf uses.
189    pub fn for_provider(crypto: &impl OpenMlsCrypto) -> Self {
190        Capabilities {
191            ciphersuites: crypto
192                .supported_ciphersuites()
193                .into_iter()
194                .map(VerifiableCiphersuite::from)
195                .collect(),
196            ..Default::default()
197        }
198    }
199
200    // ---------------------------------------------------------------------------------------------
201
202    /// Get a reference to the list of versions in this extension.
203    pub fn versions(&self) -> &[ProtocolVersion] {
204        &self.versions
205    }
206
207    /// Get a reference to the list of ciphersuites in this extension.
208    pub fn ciphersuites(&self) -> &[VerifiableCiphersuite] {
209        &self.ciphersuites
210    }
211
212    /// Get a reference to the list of supported extensions.
213    pub fn extensions(&self) -> &[ExtensionType] {
214        &self.extensions
215    }
216
217    /// Get a reference to the list of supported proposals.
218    pub fn proposals(&self) -> &[ProposalType] {
219        &self.proposals
220    }
221
222    /// Get a reference to the list of supported credential types.
223    pub fn credentials(&self) -> &[CredentialType] {
224        &self.credentials
225    }
226
227    // ---------------------------------------------------------------------------------------------
228
229    /// Check if these [`Capabilities`] support all the capabilities required by
230    /// the given [`RequiredCapabilitiesExtension`].
231    ///
232    /// # Errors
233    ///
234    /// Returns a [`LeafNodeValidationError`] error if any of the required
235    /// capabilities is not supported.
236    pub(crate) fn supports_required_capabilities(
237        &self,
238        required_capabilities: &RequiredCapabilitiesExtension,
239    ) -> Result<(), LeafNodeValidationError> {
240        // The required capabilities come from the wire, so all three checks use
241        // a set lookup and stop at the first unsupported entry.
242
243        // Check if all required extensions are supported.
244        let supported_extensions: HashSet<ExtensionType> =
245            self.extensions().iter().copied().collect();
246        if required_capabilities
247            .extension_types()
248            .iter()
249            .any(|e| !e.is_default() && !supported_extensions.contains(e))
250        {
251            log::error!(
252                "Leaf node does not support all required extension types\n
253                Supported extensions: {:?}\n
254                Required extensions: {:?}",
255                self.extensions(),
256                required_capabilities.extension_types()
257            );
258            return Err(LeafNodeValidationError::UnsupportedExtensions);
259        }
260        // Check if all required proposals are supported.
261        let supported_proposals: HashSet<ProposalType> = self.proposals().iter().copied().collect();
262        if required_capabilities
263            .proposal_types()
264            .iter()
265            .any(|p| !p.is_default() && !supported_proposals.contains(p))
266        {
267            return Err(LeafNodeValidationError::UnsupportedProposals);
268        }
269        // Check if all required credential types are supported.
270        let supported_credentials: HashSet<CredentialType> =
271            self.credentials().iter().copied().collect();
272        if required_capabilities
273            .credential_types()
274            .iter()
275            .any(|c| !supported_credentials.contains(c))
276        {
277            return Err(LeafNodeValidationError::UnsupportedCredentials);
278        }
279        Ok(())
280    }
281
282    /// Check if these [`Capabilities`] contain all the extensions.
283    pub(crate) fn contains_extensions(
284        &self,
285        extensions: &Extensions<impl ExtensionValidator>,
286    ) -> bool {
287        let mut required = extensions
288            .iter()
289            .map(Extension::extension_type)
290            .filter(|e| !e.is_default())
291            .peekable();
292
293        // Most leaf nodes carry no non-default extensions. Skip building the
294        // lookup set for them.
295        if required.peek().is_none() {
296            return true;
297        }
298
299        let supported: HashSet<ExtensionType> = self.extensions().iter().copied().collect();
300        required.all(|e| supported.contains(&e))
301    }
302
303    /// Check if these [`Capabilities`] contain the extension.
304    #[cfg(test)]
305    pub(crate) fn contains_extension_type(&self, extension: &ExtensionType) -> bool {
306        // Many leaf nodes carry no non-default extensions. Skip building the
307        // lookup set for them.
308        if extension.is_default() {
309            return true;
310        }
311
312        let supported: HashSet<ExtensionType> = self.extensions().iter().copied().collect();
313        supported.contains(extension)
314    }
315
316    /// Check if these [`Capabilities`] contains the credential.
317    pub(crate) fn contains_credential(&self, credential_type: CredentialType) -> bool {
318        self.credentials().contains(&credential_type)
319    }
320
321    /// Check if these [`Capabilities`] contain the version.
322    pub(crate) fn contains_version(&self, version: ProtocolVersion) -> bool {
323        self.versions().contains(&version)
324    }
325
326    /// Check if these [`Capabilities`] contain the ciphersuite.
327    pub(crate) fn contains_ciphersuite(&self, ciphersuite: VerifiableCiphersuite) -> bool {
328        self.ciphersuites().contains(&ciphersuite)
329    }
330
331    /// Ensure `version` is advertised.
332    ///
333    /// This is deliberately outside [`CapabilitiesPolicy`]: OpenMLS only ever
334    /// builds `Mls10` leaves, so there is no choice for a caller to get wrong
335    /// and nothing to reject. Real per-version negotiation would need to
336    /// revisit this.
337    pub(super) fn ensure_version(&mut self, version: ProtocolVersion) {
338        if !self.contains_version(version) {
339            self.versions.push(version);
340        }
341    }
342
343    /// Widen `self` to cover the non-default extension types of the
344    /// KeyPackage this leaf is built for.
345    ///
346    /// RFC 9420 requires a KeyPackage's extensions to be listed in its leaf's
347    /// capabilities, but [`Self::widen_for`] only sees the leaf's own
348    /// extensions. As there, every added entry is a fact about what is being
349    /// built.
350    pub(crate) fn widen_for_key_package(
351        &mut self,
352        key_package_extensions: &Extensions<KeyPackage>,
353    ) {
354        self.widen_for_extensions(key_package_extensions);
355    }
356
357    /// Widen `self` to cover what the leaf itself uses: its ciphersuite, its
358    /// credential type, and its non-default extension types.
359    ///
360    /// Only ever adds; never removes anything the caller set. Every added
361    /// entry is a fact about the leaf being built, so advertising it is
362    /// always truthful.
363    ///
364    /// Note what this deliberately does *not* do: it never adds the group's
365    /// [`RequiredCapabilitiesExtension`] entries. Those would be claims about
366    /// what the *application* implements, which the library is in no position
367    /// to make on its behalf, so a leaf that doesn't meet the group's
368    /// requirements is rejected instead — see
369    /// [`Capabilities::supports_required_capabilities`].
370    ///
371    /// Default extension types are skipped: RFC 9420 makes support for
372    /// them implicit, so they never need listing. GREASE extension types are
373    /// never default, so a GREASE extension actually present on the leaf is
374    /// covered here.
375    pub(super) fn widen_for(
376        &mut self,
377        ciphersuite: Ciphersuite,
378        credential_type: CredentialType,
379        leaf_extensions: &Extensions<LeafNode>,
380    ) {
381        let verifiable_ciphersuite = VerifiableCiphersuite::from(ciphersuite);
382        if !self.contains_ciphersuite(verifiable_ciphersuite) {
383            self.ciphersuites.push(verifiable_ciphersuite);
384        }
385
386        if !self.contains_credential(credential_type) {
387            self.credentials.push(credential_type);
388        }
389
390        self.widen_for_extensions(leaf_extensions);
391    }
392
393    fn widen_for_extensions(&mut self, extensions: &Extensions<impl ExtensionValidator>) {
394        for extension_type in extensions.iter().map(Extension::extension_type) {
395            if !extension_type.is_default() && !self.extensions.contains(&extension_type) {
396                self.extensions.push(extension_type);
397            }
398        }
399    }
400
401    /// Add random GREASE values to the capabilities to ensure extensibility.
402    ///
403    /// This adds one random GREASE value to each capability list if no GREASE
404    /// value is already present:
405    /// - Ciphersuites
406    /// - Extensions
407    /// - Proposals
408    /// - Credentials
409    ///
410    /// GREASE values are used per [RFC 9420 Section 13.5](https://www.rfc-editor.org/rfc/rfc9420.html#section-13.5)
411    /// to help prevent extensibility failures by ensuring implementations properly
412    /// handle unknown values.
413    ///
414    /// # Example
415    ///
416    /// ```
417    /// use openmls::prelude::*;
418    /// use openmls_rust_crypto::OpenMlsRustCrypto;
419    ///
420    /// let provider = OpenMlsRustCrypto::default();
421    ///
422    /// // Create capabilities with GREASE values injected
423    /// let capabilities = Capabilities::builder()
424    ///     .build()
425    ///     .with_grease(provider.rand());
426    ///
427    /// // Verify GREASE values were added
428    /// assert!(capabilities.ciphersuites().iter().any(|cs| cs.is_grease()));
429    /// assert!(capabilities.extensions().iter().any(|ext| ext.is_grease()));
430    /// assert!(capabilities.proposals().iter().any(|prop| prop.is_grease()));
431    /// assert!(capabilities.credentials().iter().any(|cred| cred.is_grease()));
432    /// ```
433    pub fn with_grease(mut self, rand: &impl openmls_traits::random::OpenMlsRand) -> Self {
434        use crate::credentials::CredentialType;
435        use crate::extensions::ExtensionType;
436        use crate::messages::proposals::ProposalType;
437        use openmls_traits::types::VerifiableCiphersuite;
438
439        // Add GREASE ciphersuite if none present
440        if !self.ciphersuites.iter().any(|cs| cs.is_grease()) {
441            let grease_cs = VerifiableCiphersuite::new(crate::grease::random_grease_value(rand));
442            self.ciphersuites.push(grease_cs);
443        }
444
445        // Add GREASE extension if none present
446        if !self.extensions.iter().any(|ext| ext.is_grease()) {
447            let grease_ext = ExtensionType::Grease(crate::grease::random_grease_value(rand));
448            self.extensions.push(grease_ext);
449        }
450
451        // Add GREASE proposal if none present
452        if !self.proposals.iter().any(|prop| prop.is_grease()) {
453            let grease_prop = ProposalType::Grease(crate::grease::random_grease_value(rand));
454            self.proposals.push(grease_prop);
455        }
456
457        // Add GREASE credential if none present
458        if !self.credentials.iter().any(|cred| cred.is_grease()) {
459            let grease_cred = CredentialType::Grease(crate::grease::random_grease_value(rand));
460            self.credentials.push(grease_cred);
461        }
462
463        self
464    }
465}
466
467/// A helper for building [`Capabilities`]
468#[derive(Debug, Clone)]
469pub struct CapabilitiesBuilder(Capabilities);
470
471impl CapabilitiesBuilder {
472    /// Sets the `versions` field on the [`Capabilities`].
473    pub fn versions(self, versions: Vec<ProtocolVersion>) -> Self {
474        Self(Capabilities { versions, ..self.0 })
475    }
476
477    /// Sets the `ciphersuites` field on the [`Capabilities`].
478    pub fn ciphersuites(self, ciphersuites: Vec<Ciphersuite>) -> Self {
479        let ciphersuites = ciphersuites.into_iter().map(|cs| cs.into()).collect();
480
481        Self(Capabilities {
482            ciphersuites,
483            ..self.0
484        })
485    }
486
487    /// Sets the `extensions` field on the [`Capabilities`].
488    pub fn extensions(self, extensions: Vec<ExtensionType>) -> Self {
489        Self(Capabilities {
490            extensions,
491            ..self.0
492        })
493    }
494
495    /// Sets the `proposals` field on the [`Capabilities`].
496    pub fn proposals(self, proposals: Vec<ProposalType>) -> Self {
497        Self(Capabilities {
498            proposals,
499            ..self.0
500        })
501    }
502
503    /// Sets the `credentials` field on the [`Capabilities`].
504    pub fn credentials(self, credentials: Vec<CredentialType>) -> Self {
505        Self(Capabilities {
506            credentials,
507            ..self.0
508        })
509    }
510
511    /// Adds random GREASE values to the capabilities being built.
512    ///
513    /// This is a convenience method that calls [`Capabilities::with_grease`] on the
514    /// built capabilities. See that method for more details.
515    ///
516    /// # Example
517    ///
518    /// ```
519    /// use openmls::prelude::*;
520    /// use openmls_rust_crypto::OpenMlsRustCrypto;
521    ///
522    /// let provider = OpenMlsRustCrypto::default();
523    ///
524    /// let capabilities = Capabilities::builder()
525    ///     .with_grease(provider.rand())
526    ///     .build();
527    ///
528    /// // GREASE values were added
529    /// assert!(capabilities.ciphersuites().iter().any(|cs| cs.is_grease()));
530    /// ```
531    pub fn with_grease(self, rand: &impl openmls_traits::random::OpenMlsRand) -> Self {
532        Self(self.0.with_grease(rand))
533    }
534
535    /// Builds the [`Capabilities`].
536    pub fn build(self) -> Capabilities {
537        self.0
538    }
539}
540
541#[cfg(test)]
542impl Capabilities {
543    /// Set the versions list.
544    pub fn set_versions(&mut self, versions: Vec<ProtocolVersion>) {
545        self.versions = versions;
546    }
547
548    /// Set the ciphersuites list.
549    pub fn set_ciphersuites(&mut self, ciphersuites: Vec<VerifiableCiphersuite>) {
550        self.ciphersuites = ciphersuites;
551    }
552}
553
554impl Default for Capabilities {
555    /// Like empty, but setting the default version to MLS1.0
556    fn default() -> Self {
557        Capabilities {
558            versions: default_versions(),
559            ciphersuites: vec![],
560            extensions: vec![],
561            proposals: vec![],
562            credentials: vec![],
563        }
564    }
565}
566
567/// We have a default version because this is the only version supported by
568/// OpenMLS right now.
569///
570/// All other capabilities do not have default values.
571pub(super) fn default_versions() -> Vec<ProtocolVersion> {
572    vec![ProtocolVersion::Mls10]
573}
574
575#[cfg(test)]
576mod tests {
577    use openmls_traits::{
578        crypto::OpenMlsCrypto,
579        types::{Ciphersuite, VerifiableCiphersuite},
580    };
581    use tls_codec::{Deserialize, Serialize};
582
583    use super::Capabilities;
584    use crate::{
585        credentials::CredentialType, messages::proposals::ProposalType, prelude::ExtensionType,
586        versions::ProtocolVersion,
587    };
588
589    #[test]
590    fn that_unknown_capabilities_are_de_serialized_correctly() {
591        let versions = vec![ProtocolVersion::Mls10, ProtocolVersion::Other(999)];
592        let ciphersuites = vec![
593            Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519.into(),
594            Ciphersuite::MLS_128_DHKEMP256_AES128GCM_SHA256_P256.into(),
595            Ciphersuite::MLS_128_DHKEMX25519_CHACHA20POLY1305_SHA256_Ed25519.into(),
596            Ciphersuite::MLS_256_DHKEMX448_AES256GCM_SHA512_Ed448.into(),
597            Ciphersuite::MLS_256_DHKEMP521_AES256GCM_SHA512_P521.into(),
598            Ciphersuite::MLS_256_DHKEMX448_CHACHA20POLY1305_SHA512_Ed448.into(),
599            Ciphersuite::MLS_256_DHKEMP384_AES256GCM_SHA384_P384.into(),
600            VerifiableCiphersuite::new(0x0000),
601            // Use non-GREASE values (GREASE pattern is 0x_A_A)
602            VerifiableCiphersuite::new(0x0B0B),
603            VerifiableCiphersuite::new(0x7C7C),
604            VerifiableCiphersuite::new(0xF000),
605            VerifiableCiphersuite::new(0xFFFF),
606        ];
607
608        let extensions = vec![
609            ExtensionType::Unknown(0x0000),
610            ExtensionType::Unknown(0xFAFA),
611        ];
612
613        // Use non-GREASE values
614        let proposals = vec![ProposalType::Custom(0x7C7C)];
615
616        let credentials = vec![
617            CredentialType::Basic,
618            CredentialType::X509,
619            CredentialType::Other(0x0000),
620            // Use non-GREASE values
621            CredentialType::Other(0x7C7C),
622            CredentialType::Other(0xFFFF),
623        ];
624
625        let expected = Capabilities {
626            versions,
627            ciphersuites,
628            extensions,
629            proposals,
630            credentials,
631        };
632
633        let test_serialized = expected.tls_serialize_detached().unwrap();
634
635        let got = Capabilities::tls_deserialize_exact(test_serialized).unwrap();
636
637        assert_eq!(expected, got);
638    }
639
640    #[test]
641    fn for_provider_advertises_exactly_the_supported_ciphersuites() {
642        let crypto = openmls_rust_crypto::RustCrypto::default();
643        let capabilities = Capabilities::for_provider(&crypto);
644
645        let expected: Vec<VerifiableCiphersuite> = crypto
646            .supported_ciphersuites()
647            .into_iter()
648            .map(VerifiableCiphersuite::from)
649            .collect();
650        assert_eq!(capabilities.ciphersuites(), expected.as_slice());
651    }
652}