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}