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}