openmls/group/public_group/validation.rs
1//! This module contains validation functions for incoming messages
2//! as defined in <https://github.com/openmls/openmls/wiki/Message-validation>
3
4use std::collections::{BTreeSet, HashSet};
5
6use openmls_traits::types::VerifiableCiphersuite;
7
8use super::PublicGroup;
9use crate::extensions::RequiredCapabilitiesExtension;
10use crate::group::creation::LeafNodeLifetimePolicy;
11use crate::group::proposal_store::ProposalQueue;
12use crate::group::GroupContextExtensionsProposalValidationError;
13use crate::prelude::LibraryError;
14use crate::treesync::{errors::LeafNodeValidationError, LeafNode};
15use crate::{
16 binary_tree::array_representation::LeafNodeIndex,
17 framing::{
18 mls_auth_content_in::VerifiableAuthenticatedContentIn, ContentType, ProtocolMessage,
19 Sender, WireFormat,
20 },
21 group::{
22 errors::{ExternalCommitValidationError, ProposalValidationError, ValidationError},
23 past_secrets::MessageSecretsStore,
24 Member,
25 },
26 messages::{
27 proposals::{Proposal, ProposalOrRefType, ProposalType},
28 Commit,
29 },
30 schedule::errors::PskError,
31};
32
33use crate::treesync::errors::LifetimeError;
34
35impl PublicGroup {
36 // === Messages ===
37
38 /// Checks the following semantic validation:
39 /// - ValSem002
40 /// - ValSem003
41 /// - [valn1307](https://validation.openmls.tech/#valn1307)
42 pub(crate) fn validate_framing(
43 &self,
44 message: &ProtocolMessage,
45 ) -> Result<(), ValidationError> {
46 // ValSem002
47 if message.group_id() != self.group_id() {
48 return Err(ValidationError::WrongGroupId);
49 }
50
51 // ValSem003: Check boundaries for the epoch
52 // We differentiate depending on the content type
53 match message.content_type() {
54 // For application messages we allow messages for older epochs as well
55 ContentType::Application => {
56 if message.epoch() > self.group_context().epoch() {
57 log::error!(
58 "Wrong Epoch: message.epoch() {} > {} self.group_context().epoch()",
59 message.epoch(),
60 self.group_context().epoch()
61 );
62 return Err(ValidationError::WrongEpoch);
63 }
64 }
65 // For all other messages we only only accept the current epoch
66 _ => {
67 // https://validation.openmls.tech/#valn1307
68 if message.epoch() != self.group_context().epoch() {
69 log::error!(
70 "Wrong Epoch: message.epoch() {} != {} self.group_context().epoch()",
71 message.epoch(),
72 self.group_context().epoch()
73 );
74 return Err(ValidationError::WrongEpoch);
75 }
76 }
77 }
78
79 Ok(())
80 }
81
82 /// Checks the following semantic validation:
83 /// - ValSem004
84 /// - ValSem005
85 /// - ValSem009
86 pub(super) fn validate_verifiable_content(
87 &self,
88 verifiable_content: &VerifiableAuthenticatedContentIn,
89 message_secrets_store_option: Option<&MessageSecretsStore>,
90 ) -> Result<(), ValidationError> {
91 // ValSem004
92 let sender = verifiable_content.sender();
93 if let Sender::Member(leaf_index) = sender {
94 // If the sender is a member, it has to be in the tree, except if
95 // it's an application message. Then it might be okay if it's in an
96 // old secret tree instance.
97 let is_in_secrets_store = if let Some(mss) = message_secrets_store_option {
98 mss.epoch_has_leaf(verifiable_content.epoch(), *leaf_index)
99 } else {
100 false
101 };
102 if !self.treesync().is_leaf_in_tree(*leaf_index) && !is_in_secrets_store {
103 return Err(ValidationError::UnknownMember);
104 }
105 }
106
107 // ValSem005
108 // Application messages must always be encrypted
109 if verifiable_content.content_type() == ContentType::Application {
110 if verifiable_content.wire_format() != WireFormat::PrivateMessage {
111 return Err(ValidationError::UnencryptedApplicationMessage);
112 } else if !verifiable_content.sender().is_member() {
113 return Err(ValidationError::NonMemberApplicationMessage);
114 }
115 }
116
117 // ValSem009
118 if verifiable_content.content_type() == ContentType::Commit
119 && verifiable_content.confirmation_tag().is_none()
120 {
121 return Err(ValidationError::MissingConfirmationTag);
122 }
123
124 Ok(())
125 }
126
127 // === Proposals ===
128
129 /// Validate that all group members support the types of all proposals.
130 /// Implements check [valn0311](https://validation.openmls.tech/#valn0311)
131 pub(crate) fn validate_proposal_type_support(
132 &self,
133 proposal_queue: &ProposalQueue,
134 ) -> Result<(), ProposalValidationError> {
135 let mut leaves = self.treesync().full_leaves();
136 let Some(first_leaf) = leaves.next() else {
137 return Ok(());
138 };
139 // Initialize the capabilities intersection with the capabilities of the
140 // first leaf node.
141 let mut capabilities_intersection = first_leaf
142 .capabilities()
143 .proposals()
144 .iter()
145 .collect::<HashSet<_>>();
146 // Iterate over the remaining leaf nodes and intersect their capabilities
147 for leaf_node in leaves {
148 let leaf_capabilities_set = leaf_node.capabilities().proposals().iter().collect();
149 capabilities_intersection = capabilities_intersection
150 .intersection(&leaf_capabilities_set)
151 .cloned()
152 .collect();
153 }
154
155 // Check that the types of all proposals are supported by all members
156 for proposal in proposal_queue.queued_proposals() {
157 let proposal_type = proposal.proposal().proposal_type();
158 if matches!(proposal_type, ProposalType::Custom(_))
159 && !capabilities_intersection.contains(&proposal_type)
160 {
161 return Err(ProposalValidationError::UnsupportedProposalType);
162 }
163 }
164 Ok(())
165 }
166
167 /// Validate key uniqueness. This function implements the following checks:
168 /// - ValSem101: Add Proposal: Signature public key in proposals must be unique among proposals & members
169 /// - ValSem102: Add Proposal: Init key in proposals must be unique among proposals
170 /// - ValSem103: Add Proposal: Encryption key in proposals must be unique among proposals & members
171 /// - ValSem104: Add Proposal: Init key and encryption key must be different
172 /// - ValSem110: Update Proposal: Encryption key must be unique among proposals & members
173 /// - ValSem206: Commit: Path leaf node encryption key must be unique among proposals & members
174 /// - ValSem207: Commit: Path encryption keys must be unique among proposals & members
175 /// - [valn0111]: Verify that the following fields are unique among the members of the group: `signature_key`
176 /// - [valn0112]: Verify that the following fields are unique among the members of the group: `encryption_key`
177 ///
178 /// [valn0111]: https://validation.openmls.tech/#valn0111
179 /// [valn0112]: https://validation.openmls.tech/#valn0112
180 /// [valn1208]: https://validation.openmls.tech/#valn1208
181 pub(crate) fn validate_key_uniqueness(
182 &self,
183 proposal_queue: &ProposalQueue,
184 commit: Option<&Commit>,
185 ) -> Result<(), ProposalValidationError> {
186 let mut signature_key_set = HashSet::new();
187 let mut init_key_set = HashSet::new();
188 let mut encryption_key_set = HashSet::new();
189
190 // Handle the exceptions needed for https://validation.openmls.tech/#valn0306
191 let remove_proposals = HashSet::<LeafNodeIndex>::from_iter(
192 proposal_queue
193 .remove_proposals()
194 .map(|remove_proposal| remove_proposal.remove_proposal().removed),
195 );
196
197 // Initialize the sets with the current members, filtered by the
198 // remove proposals.
199 for Member {
200 index,
201 encryption_key,
202 signature_key,
203 ..
204 } in self.treesync().full_leave_members()
205 {
206 if !remove_proposals.contains(&index) {
207 signature_key_set.insert(signature_key);
208 encryption_key_set.insert(encryption_key);
209 }
210 }
211
212 // Collect signature keys from add proposals
213 let signature_keys = proposal_queue.add_proposals().map(|add_proposal| {
214 add_proposal
215 .add_proposal()
216 .key_package()
217 .leaf_node()
218 .signature_key()
219 .as_slice()
220 .to_vec()
221 });
222
223 // Collect encryption keys from add proposals, update proposals, the
224 // commit leaf node and path keys
225 let encryption_keys = proposal_queue
226 .add_proposals()
227 .map(|add_proposal| {
228 add_proposal
229 .add_proposal()
230 .key_package()
231 .leaf_node()
232 .encryption_key()
233 .key()
234 .as_slice()
235 .to_vec()
236 })
237 .chain(proposal_queue.update_proposals().map(|update_proposal| {
238 update_proposal
239 .update_proposal()
240 .leaf_node()
241 .encryption_key()
242 .key()
243 .as_slice()
244 .to_vec()
245 }))
246 .chain(commit.and_then(|commit| {
247 commit
248 .path
249 .as_ref()
250 .map(|path| path.leaf_node().encryption_key().as_slice().to_vec())
251 }))
252 .chain(
253 commit
254 .iter()
255 .filter_map(|commit| {
256 commit.path.as_ref().map(|path| {
257 path.nodes()
258 .iter()
259 .map(|node| node.encryption_key().as_slice().to_vec())
260 })
261 })
262 .flatten(),
263 );
264
265 // Collect init keys from add proposals
266 let init_keys = proposal_queue.add_proposals().map(|add_proposal| {
267 add_proposal
268 .add_proposal()
269 .key_package()
270 .hpke_init_key()
271 .as_slice()
272 .to_vec()
273 });
274
275 // Validate uniqueness of signature keys
276 // - ValSem101
277 // - https://validation.openmls.tech/#valn0111
278 // - https://validation.openmls.tech/#valn0305
279 // - https://validation.openmls.tech/#valn0306
280 for signature_key in signature_keys {
281 if !signature_key_set.insert(signature_key) {
282 return Err(ProposalValidationError::DuplicateSignatureKey);
283 }
284 }
285
286 // Validate uniqueness of encryption keys
287 // - ValSem103
288 // - ValSem104
289 // - ValSem110
290 // - ValSem206
291 // - ValSem207
292 // - https://validation.openmls.tech/#valn0112
293 for encryption_key in encryption_keys {
294 if init_key_set.contains(&encryption_key) {
295 return Err(ProposalValidationError::InitEncryptionKeyCollision);
296 }
297 if !encryption_key_set.insert(encryption_key) {
298 return Err(ProposalValidationError::DuplicateEncryptionKey);
299 }
300 }
301
302 // Validate uniqueness of init keys
303 // - ValSem102
304 // - ValSem104
305 for init_key in init_keys {
306 if encryption_key_set.contains(&init_key) {
307 return Err(ProposalValidationError::InitEncryptionKeyCollision);
308 }
309 if !init_key_set.insert(init_key) {
310 return Err(ProposalValidationError::DuplicateInitKey);
311 }
312 }
313
314 Ok(())
315 }
316
317 /// Validate capabilities. This function implements the following checks:
318 /// - ValSem106: Add Proposal: required capabilities
319 /// - ValSem109: Update Proposal: required capabilities
320 /// - [valn0113](https://validation.openmls.tech/#valn0113).
321 pub(crate) fn validate_capabilities(
322 &self,
323 proposal_queue: &ProposalQueue,
324 ) -> Result<(), ProposalValidationError> {
325 // ValSem106/ValSem109: Check the required capabilities of the add & update
326 // proposals This includes the following checks:
327 // - Are ciphersuite & version listed in the `Capabilities` Extension?
328 // - If a `RequiredCapabilitiesExtension` is present in the group: Is
329 // this supported by the node?
330 // - Check that all extensions are contained in the capabilities.
331 // - Check that the capabilities contain the leaf node's credential
332 // type (https://validation.openmls.tech/#valn0113).
333 // - Check that the credential type is supported by all members of the
334 // group.
335 // - Check that the capabilities field of this LeafNode indicates
336 // support for all the credential types currently in use by other
337 // members.
338
339 // Extract the leaf nodes from the add & update proposals and validate them
340 proposal_queue
341 .queued_proposals()
342 .filter_map(|p| match p.proposal() {
343 Proposal::Add(add_proposal) => Some(add_proposal.key_package().leaf_node()),
344 Proposal::Update(update_proposal) => Some(update_proposal.leaf_node()),
345 _ => None,
346 })
347 .try_for_each(|leaf_node| {
348 self.validate_leaf_node_capabilities(leaf_node)
349 .map_err(|_| ProposalValidationError::InsufficientCapabilities)
350 })
351 }
352
353 /// Validate Add proposals. This function implements the following checks:
354 /// - ValSem105: Add Proposal: Ciphersuite & protocol version must match the group
355 pub(crate) fn validate_add_proposals(
356 &self,
357 proposal_queue: &ProposalQueue,
358 ) -> Result<(), ProposalValidationError> {
359 let add_proposals = proposal_queue.add_proposals();
360
361 // We do the key package validation checks here inline
362 // https://validation.openmls.tech/#valn0501
363 for add_proposal in add_proposals {
364 // ValSem105: Check if ciphersuite and version of the group are correct:
365 // https://validation.openmls.tech/#valn0201
366 if add_proposal.add_proposal().key_package().ciphersuite() != self.ciphersuite()
367 || add_proposal.add_proposal().key_package().protocol_version() != self.version()
368 {
369 return Err(ProposalValidationError::InvalidAddProposalCiphersuiteOrVersion);
370 }
371
372 // https://validation.openmls.tech/#valn0202
373 self.validate_leaf_node(add_proposal.add_proposal().key_package().leaf_node())?;
374 }
375 Ok(())
376 }
377
378 /// Validate Remove proposals. This function implements the following checks:
379 /// - ValSem107: Remove Proposal: Removed member must be unique among proposals
380 /// - ValSem108: Remove Proposal: Removed member must be an existing group member
381 pub(crate) fn validate_remove_proposals(
382 &self,
383 proposal_queue: &ProposalQueue,
384 ) -> Result<(), ProposalValidationError> {
385 let updates_set: HashSet<_> = proposal_queue
386 .update_proposals()
387 .map(|proposal| {
388 if let Sender::Member(index) = proposal.sender() {
389 Ok(*index)
390 } else {
391 Err(ProposalValidationError::UpdateFromNonMember)
392 }
393 })
394 .collect::<Result<_, _>>()?;
395
396 let remove_proposals = proposal_queue.remove_proposals();
397
398 let mut removes_set = HashSet::new();
399
400 // https://validation.openmls.tech/#valn0701
401 for remove_proposal in remove_proposals {
402 let removed = remove_proposal.remove_proposal().removed();
403 // The node has to be a leaf in the tree
404 // ValSem108
405 if !self.treesync().is_leaf_in_tree(removed) {
406 return Err(ProposalValidationError::UnknownMemberRemoval);
407 }
408
409 // ValSem107
410 // https://validation.openmls.tech/#valn0304
411 if !removes_set.insert(removed) {
412 return Err(ProposalValidationError::DuplicateMemberRemoval);
413 }
414 if updates_set.contains(&removed) {
415 return Err(ProposalValidationError::DuplicateMemberRemoval);
416 }
417
418 // removed node can not be blank
419 if self.treesync().leaf(removed).is_none() {
420 return Err(ProposalValidationError::UnknownMemberRemoval);
421 }
422 }
423
424 Ok(())
425 }
426
427 /// Validate Update proposals. This function implements the following checks:
428 /// - ValSem111: Update Proposal: The sender of a full Commit must not include own update proposals
429 /// - ValSem112: Update Proposal: The sender of a standalone update proposal must be of type member
430 ///
431 /// TODO: #133 This validation must be updated according to Sec. 13.2
432 pub(crate) fn validate_update_proposals(
433 &self,
434 proposal_queue: &ProposalQueue,
435 committer: LeafNodeIndex,
436 ) -> Result<(), ProposalValidationError> {
437 // Check the update proposals from the proposal queue first
438 let update_proposals = proposal_queue.update_proposals();
439
440 for update_proposal in update_proposals {
441 // ValSem112
442 // The sender of a standalone update proposal must be of type member
443 if let Sender::Member(sender_index) = update_proposal.sender() {
444 // ValSem111
445 // The sender of a full Commit must not include own update proposals
446 if committer == *sender_index {
447 return Err(ProposalValidationError::CommitterIncludedOwnUpdate);
448 }
449 } else {
450 return Err(ProposalValidationError::UpdateFromNonMember);
451 }
452
453 // https://validation.openmls.tech/#valn0601
454 self.validate_leaf_node(update_proposal.update_proposal().leaf_node())?;
455 }
456 Ok(())
457 }
458
459 /// Validate PreSharedKey proposals.
460 ///
461 /// This method implements the following checks:
462 ///
463 /// * ValSem401: The nonce of a PreSharedKeyID must have length KDF.Nh.
464 /// * ValSem402: PSK in proposal must be of type Resumption (with usage Application) or External.
465 /// * ValSem403: Proposal list must not contain multiple PreSharedKey proposals that reference the same PreSharedKeyID.
466 pub(crate) fn validate_pre_shared_key_proposals(
467 &self,
468 proposal_queue: &ProposalQueue,
469 ) -> Result<(), ProposalValidationError> {
470 // ValSem403 (1/2)
471 // TODO(#1335): Duplicate proposals are (likely) filtered.
472 // Let's do this check here until we haven't made sure.
473 let mut visited_psk_ids = BTreeSet::new();
474
475 for proposal in proposal_queue.psk_proposals() {
476 let psk_id = proposal.psk_proposal().clone().into_psk_id();
477
478 // ValSem401
479 // ValSem402
480 let psk_id = psk_id.validate_in_proposal(self.ciphersuite())?;
481
482 // ValSem403 (2/2)
483 if !visited_psk_ids.contains(&psk_id) {
484 visited_psk_ids.insert(psk_id);
485 } else {
486 return Err(PskError::Duplicate { first: psk_id }.into());
487 }
488 }
489
490 Ok(())
491 }
492
493 /// Validate constraints on an external commit. This function implements the following checks:
494 /// - ValSem240: External Commit, inline Proposals: There MUST be at least one ExternalInit proposal.
495 /// - ValSem241: External Commit, inline Proposals: There MUST be at most one ExternalInit proposal.
496 /// - ValSem242: External Commit must only cover inline proposal in allowlist (ExternalInit, Remove, PreSharedKey)
497 pub(crate) fn validate_external_commit(
498 &self,
499 proposal_queue: &ProposalQueue,
500 ) -> Result<(), ExternalCommitValidationError> {
501 // [valn0401](https://validation.openmls.tech/#valn0401)
502 let count_external_init_proposals = proposal_queue
503 .filtered_by_type(ProposalType::ExternalInit)
504 .count();
505 if count_external_init_proposals == 0 {
506 // ValSem240: External Commit, inline Proposals: There MUST be at least one ExternalInit proposal.
507 return Err(ExternalCommitValidationError::NoExternalInitProposals);
508 } else if count_external_init_proposals > 1 {
509 // ValSem241: External Commit, inline Proposals: There MUST be at most one ExternalInit proposal.
510 return Err(ExternalCommitValidationError::MultipleExternalInitProposals);
511 }
512
513 // ValSem242: External Commit must only cover inline proposal in allowlist (ExternalInit, Remove, PreSharedKey)
514 // [valn0404](https://validation.openmls.tech/#valn0404)
515 let contains_denied_proposal = proposal_queue.queued_proposals().any(|p| {
516 let is_inline = p.proposal_or_ref_type() == ProposalOrRefType::Proposal;
517 let is_allowed_type = matches!(
518 p.proposal(),
519 Proposal::ExternalInit(_)
520 | Proposal::Remove(_)
521 | Proposal::PreSharedKey(_)
522 | Proposal::Custom(_)
523 );
524 is_inline && !is_allowed_type
525 });
526 if contains_denied_proposal {
527 return Err(ExternalCommitValidationError::InvalidInlineProposals);
528 }
529
530 // If a Remove proposal is present,
531 // the credential in the LeafNode MUST present a set of
532 // identifiers that is acceptable to the application for
533 // the removed participant.
534 // This MUST be checked by the application.
535
536 Ok(())
537 }
538
539 /// Returns a [`LeafNodeValidationError`] if an [`ExtensionType`]
540 /// in `extensions` is not supported by a leaf in this tree.
541 /// Implements check [valn1001](https://validation.openmls.tech/#valn1001).
542 pub(crate) fn validate_group_context_extensions_proposal(
543 &self,
544 proposal_queue: &ProposalQueue,
545 ) -> Result<(), GroupContextExtensionsProposalValidationError> {
546 let iter = proposal_queue.filtered_by_type(ProposalType::GroupContextExtensions);
547
548 for (i, queued_proposal) in iter.enumerate() {
549 // There must at most be one group context extionsion proposal. Return an error if there are more
550 if i > 0 {
551 return Err(GroupContextExtensionsProposalValidationError::TooManyGCEProposals);
552 }
553
554 match queued_proposal.proposal() {
555 Proposal::GroupContextExtensions(extensions) => {
556 let required_capabilities_in_proposal =
557 extensions.extensions().required_capabilities();
558
559 // Prepare the empty required capabilities in case there is no
560 // RequiredCapabilitiesExtension in the proposal
561 let default_required_capabilities =
562 RequiredCapabilitiesExtension::new(&[], &[], &[]);
563
564 // If there is a RequiredCapabilitiesExtension in the proposal, validate it and
565 // use that. Otherwise, use the empty default one.
566 let required_capabilities = match required_capabilities_in_proposal {
567 Some(required_capabilities_new) => {
568 // If a group context extensions proposal updates the required capabilities, we
569 // need to check that these are satisfied for all existing members of the group.
570 self.check_extension_support(required_capabilities_new.extension_types()).map_err(|_| GroupContextExtensionsProposalValidationError::RequiredExtensionNotSupportedByAllMembers)?;
571 required_capabilities_new
572 }
573 None => &default_required_capabilities,
574 };
575
576 // Make sure that all other extensions are known to be supported, by checking
577 // that they are default extensions or included in the required capabilities.
578 let all_extensions_are_in_required_capabilities: bool = extensions
579 .extensions()
580 .iter()
581 .map(|ext| ext.extension_type())
582 .all(|ext_type| {
583 ext_type.is_default()
584 || required_capabilities.requires_extension_type_support(ext_type)
585 });
586
587 if !all_extensions_are_in_required_capabilities {
588 return Err(GroupContextExtensionsProposalValidationError::ExtensionNotInRequiredCapabilities);
589 }
590 }
591 _ => {
592 return Err(GroupContextExtensionsProposalValidationError::LibraryError(
593 LibraryError::custom(
594 "found non-gce proposal when filtered for gce proposals",
595 ),
596 ))
597 }
598 }
599 }
600
601 Ok(())
602 }
603
604 fn validate_leaf_node_capabilities(
605 &self,
606 leaf_node: &LeafNode,
607 ) -> Result<(), LeafNodeValidationError> {
608 // Check that the data in the leaf node is self-consistent
609 // Check that the capabilities contain the leaf node's credential
610 // type (https://validation.openmls.tech/#valn0113)
611 leaf_node.validate_locally()?;
612
613 // Check if the ciphersuite and the version of the group are
614 // supported.
615 let capabilities = leaf_node.capabilities();
616 if !capabilities.contains_ciphersuite(VerifiableCiphersuite::from(self.ciphersuite()))
617 || !capabilities.contains_version(self.version())
618 {
619 return Err(LeafNodeValidationError::CiphersuiteNotInCapabilities);
620 }
621
622 // If there is a required capabilities extension, check if that one
623 // is supported (https://validation.openmls.tech/#valn0103).
624 if let Some(required_capabilities) =
625 self.group_context().extensions().required_capabilities()
626 {
627 // Check if all required capabilities are supported.
628 capabilities.supports_required_capabilities(required_capabilities)?;
629 }
630
631 // Check that the credential type is supported by all members of the group (https://validation.openmls.tech/#valn0104).
632 if !self.treesync().full_leaves().all(|node| {
633 node.capabilities()
634 .contains_credential(leaf_node.credential().credential_type())
635 }) {
636 return Err(LeafNodeValidationError::UnsupportedCredentials);
637 }
638
639 // Check that the capabilities field of this LeafNode indicates
640 // support for all the credential types currently in use by other
641 // members (https://validation.openmls.tech/#valn0104).
642 if !self
643 .treesync()
644 .full_leaves()
645 .all(|node| capabilities.contains_credential(node.credential().credential_type()))
646 {
647 return Err(LeafNodeValidationError::UnsupportedCredentials);
648 }
649
650 Ok(())
651 }
652
653 /// Validate a leaf node.
654 ///
655 /// This always validates the lifetime.
656 pub(crate) fn validate_leaf_node(
657 &self,
658 leaf_node: &crate::treesync::LeafNode,
659 ) -> Result<(), LeafNodeValidationError> {
660 // Call the validation function and validate the lifetime
661 self.validate_leaf_node_inner(leaf_node, LeafNodeLifetimePolicy::Verify)
662 }
663
664 /// Validate a leaf node.
665 ///
666 /// This may skip checking the lifetime when validating a ratchet tree.
667 pub(crate) fn validate_leaf_node_inner(
668 &self,
669 leaf_node: &crate::treesync::LeafNode,
670 validate_lifetimes: LeafNodeLifetimePolicy,
671 ) -> Result<(), LeafNodeValidationError> {
672 // https://validation.openmls.tech/#valn0103
673 // https://validation.openmls.tech/#valn0104
674 // https://validation.openmls.tech/#valn0107
675 self.validate_leaf_node_capabilities(leaf_node)?;
676
677 // https://validation.openmls.tech/#valn0105 is done when sending
678
679 // https://validation.openmls.tech/#valn0106
680 //
681 // Only leaf nodes in key packages contain lifetimes, so this will return None for other
682 // cases. Therefore we only check the lifetimes for leaf nodes in key packages.
683 //
684 // We may want to check these in ratchet trees as well.
685 // However, this may lead to errors when leaf nodes don't get updated
686 // after being added to the tree. RFC 9420 recommends checking the lifetime
687 // but acknowledges already that this may cause issues.
688 // https://www.rfc-editor.org/rfc/rfc9420.html#section-7.3-4.5.1
689 // See #1810 for more background.
690 // We therefore check the lifetime by default, but skip it if ...
691 //
692 // Some KATs use key packages that are expired by now. In order to run these tests, we
693 // provide a way to turn off this check.
694 if matches!(validate_lifetimes, LeafNodeLifetimePolicy::Verify)
695 && !crate::skip_validation::is_disabled::leaf_node_lifetime()
696 {
697 if let Some(lifetime) = leaf_node.life_time() {
698 if !lifetime.is_valid() {
699 log::warn!(
700 "offending lifetime: {lifetime:?} for leaf node with {credential:?}",
701 credential = leaf_node.credential()
702 );
703 return Err(LeafNodeValidationError::Lifetime(LifetimeError::NotCurrent));
704 }
705 }
706 }
707
708 // These are done at the caller and we can't do them here:
709 //
710 // https://validation.openmls.tech/#valn0108
711 // https://validation.openmls.tech/#valn0109
712 // https://validation.openmls.tech/#valn0110
713
714 // These are done in validate_key_uniqueness, which is called in the context of changing
715 // this group:
716 //
717 // https://validation.openmls.tech/#valn0111
718 // https://validation.openmls.tech/#valn0112
719
720 Ok(())
721 }
722
723 /// Returns a [`LeafNodeValidationError`] if an [`ExtensionType`]
724 /// in `extensions` is not supported by a leaf in this tree.
725 pub(crate) fn check_extension_support(
726 &self,
727 extensions: &[crate::extensions::ExtensionType],
728 ) -> Result<(), LeafNodeValidationError> {
729 for leaf in self.treesync().full_leaves() {
730 leaf.check_extension_support(extensions)?;
731 }
732 Ok(())
733 }
734}