Skip to main content

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::{
10    binary_tree::array_representation::LeafNodeIndex,
11    ciphersuite::signature::SignaturePublicKey,
12    extensions::RequiredCapabilitiesExtension,
13    framing::{
14        mls_auth_content_in::VerifiableAuthenticatedContentIn, ContentType, ProtocolMessage,
15        Sender, WireFormat,
16    },
17    group::{
18        creation::LeafNodeLifetimePolicy,
19        errors::{ExternalCommitValidationError, ProposalValidationError, ValidationError},
20        past_secrets::MessageSecretsStore,
21        proposal_store::ProposalQueue,
22        GroupContextExtensionsProposalValidationError, Member,
23    },
24    key_packages::KeyPackage,
25    messages::{
26        proposals::{Proposal, ProposalOrRefType, ProposalType},
27        Commit,
28    },
29    prelude::LibraryError,
30    schedule::{errors::PskError, psk::ResumptionPskUsage, Psk},
31    treesync::{errors::LeafNodeValidationError, LeafNode},
32};
33
34#[cfg(feature = "extensions-draft")]
35use crate::{
36    group::errors::AppDataUpdateValidationError, messages::proposals::AppDataUpdateOperationType,
37};
38
39impl PublicGroup {
40    // === Messages ===
41
42    /// Checks the following semantic validation:
43    ///  - ValSem002
44    ///  - ValSem003
45    ///  - [valn1307](https://validation.openmls.tech/#valn1307)
46    pub(crate) fn validate_framing(
47        &self,
48        message: &ProtocolMessage,
49    ) -> Result<(), ValidationError> {
50        // ValSem002
51        if message.group_id() != self.group_id() {
52            return Err(ValidationError::WrongGroupId);
53        }
54
55        // ValSem003: Check boundaries for the epoch
56        // We differentiate depending on the content type
57        match message.content_type() {
58            // For application messages we allow messages for older epochs as well
59            ContentType::Application => {
60                if message.epoch() > self.group_context().epoch() {
61                    log::error!(
62                        "Wrong Epoch: message.epoch() {} > {} self.group_context().epoch()",
63                        message.epoch(),
64                        self.group_context().epoch()
65                    );
66                    return Err(ValidationError::WrongEpoch);
67                }
68            }
69            // For all other messages we only only accept the current epoch
70            _ => {
71                // https://validation.openmls.tech/#valn1307
72                if message.epoch() != self.group_context().epoch() {
73                    log::error!(
74                        "Wrong Epoch: message.epoch() {} != {} self.group_context().epoch()",
75                        message.epoch(),
76                        self.group_context().epoch()
77                    );
78                    return Err(ValidationError::WrongEpoch);
79                }
80            }
81        }
82
83        Ok(())
84    }
85
86    /// Checks the following semantic validation:
87    ///  - ValSem004
88    ///  - ValSem005
89    ///  - ValSem009
90    pub(super) fn validate_verifiable_content(
91        &self,
92        verifiable_content: &VerifiableAuthenticatedContentIn,
93        message_secrets_store_option: Option<&MessageSecretsStore>,
94    ) -> Result<(), ValidationError> {
95        // ValSem004
96        let sender = verifiable_content.sender();
97        if let Sender::Member(leaf_index) = sender {
98            // If the sender is a member, it has to be in the tree, except if
99            // it's an application message. Then it might be okay if it's in an
100            // old secret tree instance.
101            let is_in_secrets_store = if let Some(mss) = message_secrets_store_option {
102                mss.epoch_has_leaf(verifiable_content.epoch(), *leaf_index)
103            } else {
104                false
105            };
106            if !self.treesync().is_leaf_in_tree(*leaf_index) && !is_in_secrets_store {
107                return Err(ValidationError::UnknownMember);
108            }
109        }
110
111        // ValSem005
112        // Application messages must always be encrypted
113        if verifiable_content.content_type() == ContentType::Application {
114            if verifiable_content.wire_format() != WireFormat::PrivateMessage {
115                return Err(ValidationError::UnencryptedApplicationMessage);
116            } else if !verifiable_content.sender().is_member() {
117                return Err(ValidationError::NonMemberApplicationMessage);
118            }
119        }
120
121        // ValSem009
122        if verifiable_content.content_type() == ContentType::Commit
123            && verifiable_content.confirmation_tag().is_none()
124        {
125            return Err(ValidationError::MissingConfirmationTag);
126        }
127
128        Ok(())
129    }
130
131    // === Proposals ===
132
133    /// Validate that all group members support the types of all proposals.
134    /// Implements check [valn0311](https://validation.openmls.tech/#valn0311)
135    pub(crate) fn validate_proposal_type_support(
136        &self,
137        proposal_queue: &ProposalQueue,
138    ) -> Result<(), ProposalValidationError> {
139        // Collect signature keys of removed members s.t. we can skip them
140        // when checking capabilities.
141        let signature_keys: HashSet<_> = proposal_queue
142            .remove_proposals()
143            .filter_map(|p| {
144                let removed_index = p.remove_proposal().removed();
145                self.treesync()
146                    .leaf(removed_index)
147                    .map(|leaf_node| leaf_node.signature_key())
148            })
149            .collect();
150
151        // Iterate over all leaf nodes except the removed ones
152        let mut leaves = self
153            .treesync()
154            .full_leaves()
155            .filter(|(_, leaf_node)| !signature_keys.contains(leaf_node.signature_key()));
156        let Some((_, first_leaf)) = leaves.next() else {
157            return Ok(());
158        };
159
160        // Initialize the capabilities intersection with the capabilities of the
161        // first leaf node.
162        let mut capabilities_intersection = first_leaf
163            .capabilities()
164            .proposals()
165            .iter()
166            .collect::<HashSet<_>>();
167        // Iterate over the remaining leaf nodes and intersect their capabilities
168        for (_, leaf_node) in leaves {
169            let leaf_capabilities_set = leaf_node.capabilities().proposals().iter().collect();
170            capabilities_intersection = capabilities_intersection
171                .intersection(&leaf_capabilities_set)
172                .cloned()
173                .collect();
174        }
175
176        // Check that the types of all non-default proposals are supported by all members
177        for proposal in proposal_queue.queued_proposals() {
178            let proposal_type = proposal.proposal().proposal_type();
179            if !proposal_type.is_default() && !capabilities_intersection.contains(&proposal_type) {
180                return Err(ProposalValidationError::UnsupportedProposalType);
181            }
182        }
183        Ok(())
184    }
185
186    /// Validate key uniqueness. This function implements the following checks:
187    ///  - ValSem101: Add Proposal: Signature public key in proposals must be unique among proposals & members
188    ///  - ValSem102: Add Proposal: Init key in proposals must be unique among proposals
189    ///  - ValSem103: Add Proposal: Encryption key in proposals must be unique among proposals & members
190    ///  - ValSem104: Add Proposal: Init key and encryption key must be different
191    ///  - ValSem110: Update Proposal: Encryption key must be unique among proposals & members
192    ///  - ValSem206: Commit: Path leaf node encryption key must be unique among proposals & members
193    ///  - ValSem207: Commit: Path encryption keys must be unique among proposals & members
194    ///  - [valn0111]: Verify that the following fields are unique among the members of the group: `signature_key`
195    ///  - [valn0112]: Verify that the following fields are unique among the members of the group: `encryption_key`
196    ///
197    /// `path_leaf_signature_key` is the signature key of the leaf node the
198    /// commit's path installs for `sender`. It is passed separately from
199    /// `commit` because the path of a commit under construction does not exist
200    /// yet. The signature key of its leaf node is already fixed, though.
201    ///
202    /// [valn0111]: https://validation.openmls.tech/#valn0111
203    /// [valn0112]: https://validation.openmls.tech/#valn0112
204    /// [valn1208]: https://validation.openmls.tech/#valn1208
205    pub(crate) fn validate_key_uniqueness(
206        &self,
207        proposal_queue: &ProposalQueue,
208        commit: Option<&Commit>,
209        sender: &Sender,
210        path_leaf_signature_key: Option<&SignaturePublicKey>,
211    ) -> Result<(), ProposalValidationError> {
212        let mut signature_key_set = HashSet::new();
213        let mut init_key_set = HashSet::new();
214        let mut encryption_key_set = HashSet::new();
215
216        // Handle the exceptions needed for https://validation.openmls.tech/#valn0306
217        let removed_members = proposal_queue
218            .remove_proposals()
219            .map(|remove_proposal| remove_proposal.remove_proposal().removed)
220            .chain(
221                proposal_queue
222                    .filtered_by_type(ProposalType::SelfRemove)
223                    .filter_map(|self_remove| self_remove.sender().as_member()),
224            )
225            .collect::<HashSet<LeafNodeIndex>>();
226
227        // The leaf node in the path replaces the committer's leaf, so the
228        // committer's current signature key is not compared with it.
229        let replaced_leaf = path_leaf_signature_key.and_then(|_| sender.as_member());
230
231        // Initialize the sets with the current members, filtered by the
232        // remove proposals.
233        for Member {
234            index,
235            encryption_key,
236            signature_key,
237            ..
238        } in self.treesync().full_leaf_members()
239        {
240            if removed_members.contains(&index) {
241                continue;
242            }
243            encryption_key_set.insert(encryption_key);
244            if replaced_leaf != Some(index) {
245                signature_key_set.insert(signature_key);
246            }
247        }
248
249        // Collect signature keys from add proposals and the commit path leaf
250        // node
251        let signature_keys = proposal_queue
252            .add_proposals()
253            .map(|add_proposal| {
254                add_proposal
255                    .add_proposal()
256                    .key_package()
257                    .leaf_node()
258                    .signature_key()
259                    .as_slice()
260                    .to_vec()
261            })
262            .chain(path_leaf_signature_key.map(|signature_key| signature_key.as_slice().to_vec()));
263
264        // Collect encryption keys from add proposals, update proposals, the
265        // commit leaf node and path keys
266        let encryption_keys = proposal_queue
267            .add_proposals()
268            .map(|add_proposal| {
269                add_proposal
270                    .add_proposal()
271                    .key_package()
272                    .leaf_node()
273                    .encryption_key()
274                    .key()
275                    .as_slice()
276                    .to_vec()
277            })
278            .chain(proposal_queue.update_proposals().map(|update_proposal| {
279                update_proposal
280                    .update_proposal()
281                    .leaf_node()
282                    .encryption_key()
283                    .key()
284                    .as_slice()
285                    .to_vec()
286            }))
287            .chain(commit.and_then(|commit| {
288                commit
289                    .path
290                    .as_ref()
291                    .map(|path| path.leaf_node().encryption_key().as_slice().to_vec())
292            }))
293            .chain(
294                commit
295                    .iter()
296                    .filter_map(|commit| {
297                        commit.path.as_ref().map(|path| {
298                            path.nodes()
299                                .iter()
300                                .map(|node| node.encryption_key().as_slice().to_vec())
301                        })
302                    })
303                    .flatten(),
304            );
305
306        // Collect init keys from add proposals
307        let init_keys = proposal_queue.add_proposals().map(|add_proposal| {
308            add_proposal
309                .add_proposal()
310                .key_package()
311                .hpke_init_key()
312                .as_slice()
313                .to_vec()
314        });
315
316        // Validate uniqueness of signature keys
317        //  - ValSem101
318        //  - https://validation.openmls.tech/#valn0111
319        //  - https://validation.openmls.tech/#valn0305
320        //  - https://validation.openmls.tech/#valn0306
321        //  - https://validation.openmls.tech/#valn1207
322        for signature_key in signature_keys {
323            if !signature_key_set.insert(signature_key) {
324                return Err(ProposalValidationError::DuplicateSignatureKey);
325            }
326        }
327
328        // Validate uniqueness of encryption keys
329        //  - ValSem103
330        //  - ValSem104
331        //  - ValSem110
332        //  - ValSem206
333        //  - ValSem207
334        //  - https://validation.openmls.tech/#valn0112
335        //  - https://validation.openmls.tech/#valn1209
336        for encryption_key in encryption_keys {
337            if init_key_set.contains(&encryption_key) {
338                return Err(ProposalValidationError::InitEncryptionKeyCollision);
339            }
340            if !encryption_key_set.insert(encryption_key) {
341                return Err(ProposalValidationError::DuplicateEncryptionKey);
342            }
343        }
344
345        // Validate uniqueness of init keys
346        //  - ValSem102
347        //  - ValSem104
348        for init_key in init_keys {
349            if encryption_key_set.contains(&init_key) {
350                return Err(ProposalValidationError::InitEncryptionKeyCollision);
351            }
352            if !init_key_set.insert(init_key) {
353                return Err(ProposalValidationError::DuplicateInitKey);
354            }
355        }
356
357        Ok(())
358    }
359
360    /// Validate capabilities. This function implements the following checks:
361    /// - ValSem106: Add Proposal: required capabilities
362    /// - ValSem109: Update Proposal: required capabilities
363    /// - [valn0113](https://validation.openmls.tech/#valn0113).
364    pub(crate) fn validate_capabilities(
365        &self,
366        proposal_queue: &ProposalQueue,
367    ) -> Result<(), ProposalValidationError> {
368        // ValSem106/ValSem109: Check the required capabilities of the add & update
369        // proposals This includes the following checks:
370        // - Are ciphersuite & version listed in the `Capabilities` Extension?
371        // - If a `RequiredCapabilitiesExtension` is present in the group: Is
372        //   this supported by the node?
373        // - Check that all extensions are contained in the capabilities.
374        // - Check that the capabilities contain the leaf node's credential
375        //   type (https://validation.openmls.tech/#valn0113).
376        // - Check that the credential type is supported by all members of the
377        //   group.
378        // - Check that the capabilities field of this LeafNode indicates
379        //   support for all the credential types currently in use by other
380        //   members.
381
382        // Extract the leaf nodes from the add & update proposals and validate them
383        proposal_queue
384            .queued_proposals()
385            .filter_map(|p| match p.proposal() {
386                Proposal::Add(add_proposal) => Some(add_proposal.key_package().leaf_node()),
387                Proposal::Update(update_proposal) => Some(update_proposal.leaf_node()),
388                _ => None,
389            })
390            .try_for_each(|leaf_node| {
391                self.validate_leaf_node_capabilities(leaf_node)
392                    .map_err(|_| ProposalValidationError::InsufficientCapabilities)
393            })
394    }
395
396    /// Checks whether `key_package` is eligible to be added to this group
397    ///
398    /// This runs the checks that a commit with an Add proposal for
399    /// `key_package` performs on that key package alone:
400    ///
401    /// - the ciphersuite and the protocol version match the group,
402    /// - the leaf node supports all extensions in the group context,
403    /// - the leaf node is valid for this group, which covers its capabilities,
404    ///   the required capabilities of the group, mutual support of the
405    ///   credential types in use with the existing members, and the lifetime of
406    ///   the leaf node.
407    ///
408    /// Checks that concern a set of proposals as a whole are not covered. In
409    /// particular the signature key, the init key and the encryption key of the
410    /// added member have to be unique among the group members and the other
411    /// proposals in the commit, which can only be decided once all proposals are
412    /// known. Passing this check therefore does not guarantee that a commit
413    /// adding `key_package` can be built.
414    pub fn validate_key_package_for_add(
415        &self,
416        key_package: &KeyPackage,
417    ) -> Result<(), ProposalValidationError> {
418        // ValSem105: Check if ciphersuite and version of the group are correct:
419        // https://validation.openmls.tech/#valn0201
420        if key_package.ciphersuite() != self.ciphersuite()
421            || key_package.protocol_version() != self.version()
422        {
423            return Err(ProposalValidationError::InvalidAddProposalCiphersuiteOrVersion);
424        }
425
426        // Check that the leaf node of the added key package supports all extensions in the group
427        // context.
428        // https://validation.openmls.tech/#valn0502
429        let added_leaf_supports_all_group_context_extensions =
430            self.group_context().extensions().iter().all(|extension| {
431                key_package
432                    .leaf_node()
433                    .supports_extension(&extension.extension_type())
434            });
435        if !added_leaf_supports_all_group_context_extensions {
436            return Err(ProposalValidationError::InsufficientCapabilities);
437        }
438
439        // https://validation.openmls.tech/#valn0202
440        self.validate_leaf_node(key_package.leaf_node())?;
441
442        Ok(())
443    }
444
445    /// Validate Add proposals. This function implements the following checks:
446    ///  - ValSem105: Add Proposal: Ciphersuite & protocol version must match the group
447    pub(crate) fn validate_add_proposals(
448        &self,
449        proposal_queue: &ProposalQueue,
450    ) -> Result<(), ProposalValidationError> {
451        let add_proposals = proposal_queue.add_proposals();
452
453        // We do the key package validation checks here inline
454        // https://validation.openmls.tech/#valn0501
455        for add_proposal in add_proposals {
456            self.validate_key_package_for_add(add_proposal.add_proposal().key_package())?;
457        }
458        Ok(())
459    }
460
461    /// Validate Remove proposals. This function implements the following checks:
462    ///  - ValSem107: Remove Proposal: Removed member must be unique among proposals
463    ///  - ValSem108: Remove Proposal: Removed member must be an existing group member
464    pub(crate) fn validate_remove_proposals(
465        &self,
466        proposal_queue: &ProposalQueue,
467    ) -> Result<(), ProposalValidationError> {
468        let updates_set: HashSet<_> = proposal_queue
469            .update_proposals()
470            .map(|proposal| {
471                if let Sender::Member(index) = proposal.sender() {
472                    Ok(*index)
473                } else {
474                    Err(ProposalValidationError::UpdateFromNonMember)
475                }
476            })
477            .collect::<Result<_, _>>()?;
478
479        let remove_proposals = proposal_queue.remove_proposals();
480
481        let mut removes_set = HashSet::new();
482
483        // https://validation.openmls.tech/#valn0701
484        for remove_proposal in remove_proposals {
485            let removed = remove_proposal.remove_proposal().removed();
486            // The node has to be a leaf in the tree
487            // ValSem108
488            if !self.treesync().is_leaf_in_tree(removed) {
489                return Err(ProposalValidationError::UnknownMemberRemoval);
490            }
491
492            // ValSem107
493            // https://validation.openmls.tech/#valn0304
494            if !removes_set.insert(removed) {
495                return Err(ProposalValidationError::DuplicateMemberRemoval);
496            }
497            if updates_set.contains(&removed) {
498                return Err(ProposalValidationError::DuplicateMemberRemoval);
499            }
500
501            // removed node can not be blank
502            if self.treesync().leaf(removed).is_none() {
503                return Err(ProposalValidationError::UnknownMemberRemoval);
504            }
505        }
506
507        Ok(())
508    }
509
510    /// Validate Update proposals. This function implements the following checks:
511    ///  - ValSem111: Update Proposal: The sender of a full Commit must not include own update proposals
512    ///  - ValSem112: Update Proposal: The sender of a standalone update proposal must be of type member
513    ///
514    /// TODO: #133 This validation must be updated according to Sec. 13.2
515    pub(crate) fn validate_update_proposals(
516        &self,
517        proposal_queue: &ProposalQueue,
518        committer: LeafNodeIndex,
519    ) -> Result<(), ProposalValidationError> {
520        // Check the update proposals from the proposal queue first
521        let update_proposals = proposal_queue.update_proposals();
522
523        for update_proposal in update_proposals {
524            // ValSem112
525            // The sender of a standalone update proposal must be of type member
526            if let Sender::Member(sender_index) = update_proposal.sender() {
527                // ValSem111
528                // https://validation.openmls.tech/#valn0302
529                // The sender of a full Commit must not include own update proposals
530                if committer == *sender_index {
531                    return Err(ProposalValidationError::CommitterIncludedOwnUpdate);
532                }
533            } else {
534                return Err(ProposalValidationError::UpdateFromNonMember);
535            }
536
537            // https://validation.openmls.tech/#valn0601
538            self.validate_leaf_node(update_proposal.update_proposal().leaf_node())?;
539
540            // Check that the leaf node in the update proposal supports all group context extensions
541            // https://validation.openmls.tech/#valn0602
542            let leaf_node_supports_group_context_extensions =
543                self.group_context().extensions().iter().all(|extension| {
544                    update_proposal
545                        .update_proposal()
546                        .leaf_node()
547                        .supports_extension(&extension.extension_type())
548                });
549
550            if !leaf_node_supports_group_context_extensions {
551                return Err(ProposalValidationError::LeafNodeValidation(
552                    LeafNodeValidationError::UnsupportedExtensions,
553                ));
554            }
555        }
556        Ok(())
557    }
558
559    /// Validate PreSharedKey proposals.
560    ///
561    /// This method implements the following checks:
562    ///
563    /// * ValSem401: The nonce of a PreSharedKeyID must have length KDF.Nh.
564    /// * ValSem402: PSK in proposal must be of type Resumption (with usage Application) or External.
565    pub(crate) fn validate_pre_shared_key_proposals(
566        &self,
567        proposal_queue: &ProposalQueue,
568    ) -> Result<(), ProposalValidationError> {
569        // ValSem403 (1/2)
570        // TODO(#1335): Duplicate proposals are (likely) filtered.
571        //              Let's do this check here until we haven't made sure.
572        let mut visited_psk_ids = BTreeSet::new();
573
574        for proposal in proposal_queue.psk_proposals() {
575            let psk_id = proposal.psk_proposal().clone().into_psk_id();
576
577            // ValSem401
578            // ValSem402
579            // https://validation.openmls.tech/#valn0803
580            let psk_id = psk_id.validate_in_proposal(self.ciphersuite())?;
581            if let Psk::Resumption(psk) = psk_id.psk() {
582                if matches!(psk.usage(), ResumptionPskUsage::Branch) {
583                    // https://validation.openmls.tech/#valn0802
584                    // Branching PSKs must only be processed as part of the
585                    // initial commit, adding the other members.
586                    if self.group_context.epoch().as_u64() != 0 {
587                        return Err(PskError::NotAllowed.into());
588                    }
589                    // Note: branch/reinit exclusivity (valn1401) is enforced for
590                    // the Welcome PSK list in `PreSharedKeyId::validate_in_welcome`.
591                }
592            }
593
594            // ValSem403 (2/2)
595            if !visited_psk_ids.contains(&psk_id) {
596                visited_psk_ids.insert(psk_id);
597            } else {
598                return Err(PskError::Duplicate { first: psk_id }.into());
599            }
600        }
601
602        Ok(())
603    }
604
605    /// Validate constraints on an external commit. This function implements the following checks:
606    ///  - ValSem240: External Commit, inline Proposals: There MUST be at least one ExternalInit proposal.
607    ///  - ValSem241: External Commit, inline Proposals: There MUST be at most one ExternalInit proposal.
608    ///  - ValSem242: External Commit must only cover inline proposal in allowlist (ExternalInit, Remove, PreSharedKey)
609    ///  - When the `extensions-draft` feature is enabled, AppDataUpdate and AppEphemeral proposals are allowed additionally.
610    pub(crate) fn validate_external_commit(
611        &self,
612        proposal_queue: &ProposalQueue,
613    ) -> Result<(), ExternalCommitValidationError> {
614        // [valn0401](https://validation.openmls.tech/#valn0401)
615        let count_external_init_proposals = proposal_queue
616            .filtered_by_type(ProposalType::ExternalInit)
617            .count();
618        if count_external_init_proposals == 0 {
619            // ValSem240: External Commit, inline Proposals: There MUST be at least one ExternalInit proposal.
620            return Err(ExternalCommitValidationError::NoExternalInitProposals);
621        } else if count_external_init_proposals > 1 {
622            // ValSem241: External Commit, inline Proposals: There MUST be at most one ExternalInit proposal.
623            return Err(ExternalCommitValidationError::MultipleExternalInitProposals);
624        }
625
626        // ValSem242: External Commit must only cover inline proposal in allowlist (ExternalInit, Remove, PreSharedKey)
627        // [valn0404](https://validation.openmls.tech/#valn0404)
628        let contains_denied_proposal = proposal_queue.queued_proposals().any(|p| {
629            let is_inline = p.proposal_or_ref_type() == ProposalOrRefType::Proposal;
630            let is_allowed_type = match p.proposal() {
631                Proposal::ExternalInit(_)
632                | Proposal::Remove(_)
633                | Proposal::PreSharedKey(_)
634                | Proposal::Custom(_) => true,
635                #[cfg(feature = "extensions-draft")]
636                Proposal::AppDataUpdate(_) | Proposal::AppEphemeral(_) => true,
637                _ => false,
638            };
639            is_inline && !is_allowed_type
640        });
641        if contains_denied_proposal {
642            return Err(ExternalCommitValidationError::InvalidInlineProposals);
643        }
644
645        // If a Remove proposal is present,
646        // the credential in the LeafNode MUST present a set of
647        // identifiers that is acceptable to the application for
648        // the removed participant.
649        // This MUST be checked by the application.
650
651        Ok(())
652    }
653
654    /// Returns a [`LeafNodeValidationError`] if an [`ExtensionType`]
655    /// in `extensions` is not supported by a leaf in this tree.
656    /// Implements check [valn1001](https://validation.openmls.tech/#valn1001).
657    pub(crate) fn validate_group_context_extensions_proposal(
658        &self,
659        proposal_queue: &ProposalQueue,
660    ) -> Result<(), GroupContextExtensionsProposalValidationError> {
661        let iter = proposal_queue.filtered_by_type(ProposalType::GroupContextExtensions);
662
663        for (i, queued_proposal) in iter.enumerate() {
664            // There must at most be one group context extionsion proposal. Return an error if there are more
665            if i > 0 {
666                return Err(GroupContextExtensionsProposalValidationError::TooManyGCEProposals);
667            }
668
669            match queued_proposal.proposal() {
670                Proposal::GroupContextExtensions(extensions) => {
671                    let required_capabilities_in_proposal =
672                        extensions.extensions().required_capabilities();
673
674                    // Prepare the empty required capabilities in case there is no
675                    // RequiredCapabilitiesExtension in the proposal
676                    let default_required_capabilities =
677                        RequiredCapabilitiesExtension::new(&[], &[], &[]);
678
679                    // If there is a RequiredCapabilitiesExtension in the proposal, validate it and
680                    // use that. Otherwise, use the empty default one.
681                    let required_capabilities = match required_capabilities_in_proposal {
682                        Some(required_capabilities_new) => {
683                            // If a group context extensions proposal updates the required capabilities, we
684                            // need to check that these are satisfied for all existing members of the group.
685                            self.check_extension_support(required_capabilities_new.extension_types()).map_err(|_| GroupContextExtensionsProposalValidationError::RequiredExtensionNotSupportedByAllMembers)?;
686                            required_capabilities_new
687                        }
688                        None => &default_required_capabilities,
689                    };
690
691                    // Make sure that all other extensions are known to be supported, by checking
692                    // that they are default extensions or included in the required capabilities.
693                    let all_extensions_are_in_required_capabilities: bool = extensions
694                        .extensions()
695                        .iter()
696                        .map(|ext| ext.extension_type())
697                        .all(|ext_type| {
698                            ext_type.is_default()
699                                || required_capabilities.requires_extension_type_support(ext_type)
700                        });
701
702                    if !all_extensions_are_in_required_capabilities {
703                        return Err(GroupContextExtensionsProposalValidationError::ExtensionNotInRequiredCapabilities);
704                    }
705                }
706                _ => {
707                    return Err(GroupContextExtensionsProposalValidationError::LibraryError(
708                        LibraryError::custom(
709                            "found non-gce proposal when filtered for gce proposals",
710                        ),
711                    ))
712                }
713            }
714        }
715
716        Ok(())
717    }
718
719    /// Returns an [`AppDataUpdateValidationError`] if:
720    ///   - An [`AppDataUpdateProposal`] appears before a [`GroupContextExtensionProposal`]
721    ///   - The [`GroupContextExtensionProposal`] updates the [`AppDataDictionary`] when the
722    ///     required capabilities include AppDataUpdate proposal type
723    ///   - For any [`ComponentId`], the list of [`AppDataUpdateProposal`]s includes both Updates
724    ///     and Removes
725    ///   - For any [`ComponentId`], the list of [`AppDataUpdateProposal`]s includes more than one
726    ///     Remove
727    #[cfg(feature = "extensions-draft")]
728    pub(crate) fn validate_app_data_update_proposals_and_group_context(
729        &self,
730        proposal_queue: &ProposalQueue,
731    ) -> Result<(), AppDataUpdateValidationError> {
732        // retrieve the GroupContextExtensions proposal, if available
733        let group_context_extension_proposal = proposal_queue
734            .filtered_by_type(ProposalType::GroupContextExtensions)
735            .filter_map(|queued_proposal| match queued_proposal.proposal() {
736                Proposal::GroupContextExtensions(p) => Some(p),
737                _ => None,
738            })
739            .next();
740
741        // From https://datatracker.ietf.org/doc/html/draft-ietf-mls-extensions#section-4.7-6:
742        // When an MLS group contains the AppDataUpdate proposal type in the proposal_types list in
743        // the group's required_capabilities extension, a GroupContextExtensions proposal MUST NOT
744        // add, remove, or modify the app_data_dictionary GroupContext extension. In other words,
745        // when every member of the group supports the AppDataUpdate proposal, a
746        // GroupContextExtensions proposal could be sent to update some other extension(s), but the
747        // app_data_dictionary GroupContext extension, if it exists, is left as it was.
748        //
749        // This is checked *before* the "no AppDataUpdate proposals" early return below: the rule
750        // binds every commit that carries a GroupContextExtensions proposal, including one with no
751        // accompanying AppDataUpdate proposals. Skipping it in that case would let a bare
752        // GroupContextExtensions proposal rewrite the app_data_dictionary directly, bypassing the
753        // AppDataUpdate machinery.
754        //
755        // The rule is treated as active when the group requires AppDataUpdate either before or
756        // after the commit. Reading only the *proposed* required_capabilities would let a sender
757        // evade the check by dropping AppDataUpdate from required_capabilities in the same
758        // proposal; reading only the *current* required_capabilities would skip the migrating
759        // commit that both introduces the dictionary and adds AppDataUpdate.
760        if let Some(group_context_extension) = group_context_extension_proposal {
761            let current_requires_app_data_update = self
762                .group_context()
763                .extensions()
764                .required_capabilities()
765                .map(|rc| rc.proposal_types().contains(&ProposalType::AppDataUpdate))
766                .unwrap_or(false);
767            let proposed_requires_app_data_update = group_context_extension
768                .extensions()
769                .required_capabilities()
770                .map(|rc| rc.proposal_types().contains(&ProposalType::AppDataUpdate))
771                .unwrap_or(false);
772
773            if (current_requires_app_data_update || proposed_requires_app_data_update)
774                && group_context_extension.extensions().app_data_dictionary()
775                    != self.group_context().extensions().app_data_dictionary()
776            {
777                return Err(AppDataUpdateValidationError::CannotUpdateDictionaryDirectly);
778            }
779        }
780
781        let no_app_data_updates = proposal_queue.app_data_update_proposals().next().is_none();
782        if no_app_data_updates {
783            return Ok(());
784        }
785
786        // check ordering
787        // return an error if an AppDataUpdate appears before a GroupContextExtensions proposal
788        //
789        // From the draft:
790        //
791        // A commit can contain a GroupContextExtensions proposal which modifies
792        // GroupContext extensions other than app_data_dictionary, and can be
793        // followed by zero or more AppDataUpdate proposals.
794        //
795        // https://datatracker.ietf.org/doc/html/draft-ietf-mls-extensions#section-4.7-7
796        //
797        // Note that it is a bit unclear if this is really needed; we already require that default
798        // proposals are processed first.
799        if proposal_queue
800            .queued_proposals()
801            .map(|proposal| proposal.proposal().proposal_type())
802            .skip_while(|proposal_type| *proposal_type != ProposalType::AppDataUpdate)
803            .any(|proposal_type| proposal_type == ProposalType::GroupContextExtensions)
804        {
805            return Err(AppDataUpdateValidationError::IncorrectOrder);
806        }
807
808        // From the draft:
809        //
810        // A proposal list is invalid if it includes multiple AppDataUpdate proposals that remove state for the same component_id, or proposals that both update and remove state for the same component_id.
811        //
812        // https://datatracker.ietf.org/doc/html/draft-ietf-mls-extensions#section-4.7-4
813        // NOTE: We depend on the proposals being sorted by component id first and by
814        // type second - for a given ID, removes come first. The sorting is stable and
815        // won't mess with the ordering of updates within a component.
816        // This is ensured in ProposalQueue::app_data_update_proposals.
817        let mut latest = None;
818        for proposal in proposal_queue.app_data_update_proposals() {
819            let proposal = &proposal.app_data_update_proposal;
820            let component_id = proposal.component_id();
821            let operation_type = proposal.operation().operation_type();
822
823            if latest == Some(component_id) {
824                let app_data_update_validation_error = match operation_type {
825                    AppDataUpdateOperationType::Update => {
826                        AppDataUpdateValidationError::CombinedRemoveAndUpdateOperations
827                    }
828                    AppDataUpdateOperationType::Remove => {
829                        AppDataUpdateValidationError::CombinedRemoveAndUpdateOperations
830                    }
831                };
832
833                return Err(app_data_update_validation_error);
834            }
835
836            if proposal.operation().operation_type() == AppDataUpdateOperationType::Remove {
837                // From the draft:
838                //
839                // An AppDataUpdate proposal is invalid if [...] it specifies the removal of
840                // state for a component_id that has no state present.
841                //
842                // https://datatracker.ietf.org/doc/html/draft-ietf-mls-extensions#section-4.7-4
843                let Some(gce) = group_context_extension_proposal else {
844                    // extension gets implicitly created in the group context, so absence is not an
845                    // error condition
846                    return Ok(());
847                };
848                let Some(app_data_dict) = gce.extensions().app_data_dictionary() else {
849                    // extension gets implicitly created in the group context, so absence is not an
850                    // error condition
851                    return Ok(());
852                };
853
854                if app_data_dict.dictionary().get(&component_id).is_none() {
855                    return Err(AppDataUpdateValidationError::CannotRemoveNonexistentComponent);
856                }
857
858                latest = Some(component_id)
859            }
860        }
861
862        Ok(())
863    }
864
865    fn validate_leaf_node_capabilities(
866        &self,
867        leaf_node: &LeafNode,
868    ) -> Result<(), LeafNodeValidationError> {
869        // Check that the data in the leaf node is self-consistent
870        // Check that the capabilities contain the leaf node's credential
871        // type (https://validation.openmls.tech/#valn0113)
872        // Check that all extension types are valid in leaf node
873        // (https://validation.openmls.tech/#valn1601)
874        leaf_node.validate_locally()?;
875
876        // Check if the ciphersuite and the version of the group are
877        // supported.
878        let capabilities = leaf_node.capabilities();
879        if !capabilities.contains_ciphersuite(VerifiableCiphersuite::from(self.ciphersuite()))
880            || !capabilities.contains_version(self.version())
881        {
882            return Err(LeafNodeValidationError::CiphersuiteNotInCapabilities);
883        }
884
885        // If there is a required capabilities extension, check if that one
886        // is supported (https://validation.openmls.tech/#valn0103).
887        if let Some(required_capabilities) =
888            self.group_context().extensions().required_capabilities()
889        {
890            // Check if all required capabilities are supported.
891            capabilities.supports_required_capabilities(required_capabilities)?;
892        }
893
894        // Check that the credential type is supported by all members of the group (https://validation.openmls.tech/#valn0104).
895        if !self.treesync().full_leaves().all(|(_, node)| {
896            node.capabilities()
897                .contains_credential(leaf_node.credential().credential_type())
898        }) {
899            return Err(LeafNodeValidationError::UnsupportedCredentials);
900        }
901
902        // Check that the capabilities field of this LeafNode indicates
903        // support for all the credential types currently in use by other
904        // members (https://validation.openmls.tech/#valn0104).
905        if !self
906            .treesync()
907            .full_leaves()
908            .all(|(_, node)| capabilities.contains_credential(node.credential().credential_type()))
909        {
910            return Err(LeafNodeValidationError::UnsupportedCredentials);
911        }
912
913        Ok(())
914    }
915
916    /// Validate a leaf node.
917    ///
918    /// This always validates the lifetime.
919    pub(crate) fn validate_leaf_node(
920        &self,
921        leaf_node: &crate::treesync::LeafNode,
922    ) -> Result<(), LeafNodeValidationError> {
923        // Call the validation function and validate the lifetime
924        self.validate_leaf_node_inner(leaf_node, LeafNodeLifetimePolicy::Verify)
925    }
926
927    /// Validate a leaf node.
928    ///
929    /// This may skip checking the lifetime when validating a ratchet tree.
930    pub(crate) fn validate_leaf_node_inner(
931        &self,
932        leaf_node: &crate::treesync::LeafNode,
933        validate_lifetimes: LeafNodeLifetimePolicy,
934    ) -> Result<(), LeafNodeValidationError> {
935        // https://validation.openmls.tech/#valn0103
936        // https://validation.openmls.tech/#valn0104
937        // https://validation.openmls.tech/#valn0107
938        self.validate_leaf_node_capabilities(leaf_node)?;
939
940        // https://validation.openmls.tech/#valn0105 is done when sending
941
942        // https://validation.openmls.tech/#valn0106
943        //
944        // Only leaf nodes in key packages contain lifetimes, so this will return None for other
945        // cases. Therefore we only check the lifetimes for leaf nodes in key packages.
946        //
947        // We may want to check these in ratchet trees as well.
948        // However, this may lead to errors when leaf nodes don't get updated
949        // after being added to the tree. RFC 9420 recommends checking the lifetime
950        // but acknowledges already that this may cause issues.
951        // https://www.rfc-editor.org/rfc/rfc9420.html#section-7.3-4.5.1
952        // See #1810 for more background.
953        // We therefore check the lifetime by default, but skip it if ...
954        //
955        // Some KATs use key packages that are expired by now. In order to run these tests, we
956        // provide a way to turn off this check.
957        if matches!(validate_lifetimes, LeafNodeLifetimePolicy::Verify)
958            && !crate::skip_validation::is_disabled::leaf_node_lifetime()
959        {
960            if let Some(lifetime) = leaf_node.life_time() {
961                lifetime.validate()?;
962            }
963        }
964
965        // These are done at the caller and we can't do them here:
966        //
967        // https://validation.openmls.tech/#valn0108
968        // https://validation.openmls.tech/#valn0109
969        // https://validation.openmls.tech/#valn0110
970
971        // These are done in validate_key_uniqueness, which is called in the context of changing
972        // this group:
973        //
974        // https://validation.openmls.tech/#valn0111
975        // https://validation.openmls.tech/#valn0112
976
977        Ok(())
978    }
979
980    /// Returns a [`LeafNodeValidationError`] if an [`ExtensionType`]
981    /// in `extensions` is not supported by a leaf in this tree.
982    pub(crate) fn check_extension_support(
983        &self,
984        extensions: &[crate::extensions::ExtensionType],
985    ) -> Result<(), LeafNodeValidationError> {
986        for (_, leaf) in self.treesync().full_leaves() {
987            leaf.check_extension_support(extensions)?;
988        }
989        Ok(())
990    }
991}