| // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. |
| // Copyright by contributors to this project. |
| // SPDX-License-Identifier: (Apache-2.0 OR MIT) |
| |
| use alloc::{vec, vec::Vec}; |
| use mls_rs_codec::{MlsDecode, MlsEncode, MlsSize}; |
| use mls_rs_core::{error::IntoAnyError, identity::IdentityProvider}; |
| |
| use super::{ |
| leaf_node::LeafNode, |
| leaf_node_validator::{LeafNodeValidator, ValidationContext}, |
| node::LeafIndex, |
| }; |
| use crate::{ |
| client::MlsError, |
| crypto::{CipherSuiteProvider, HpkeCiphertext, HpkePublicKey}, |
| }; |
| use crate::{group::message_processor::ProvisionalState, time::MlsTime}; |
| |
| #[derive(Clone, Debug, PartialEq, Eq, MlsSize, MlsEncode, MlsDecode)] |
| #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] |
| #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
| pub struct UpdatePathNode { |
| pub public_key: HpkePublicKey, |
| pub encrypted_path_secret: Vec<HpkeCiphertext>, |
| } |
| |
| #[derive(Clone, Debug, PartialEq, MlsSize, MlsEncode, MlsDecode)] |
| #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] |
| #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
| pub struct UpdatePath { |
| pub leaf_node: LeafNode, |
| pub nodes: Vec<UpdatePathNode>, |
| } |
| |
| #[derive(Clone, Debug, PartialEq)] |
| pub struct ValidatedUpdatePath { |
| pub leaf_node: LeafNode, |
| pub nodes: Vec<Option<UpdatePathNode>>, |
| } |
| |
| #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] |
| pub(crate) async fn validate_update_path<C: IdentityProvider, CSP: CipherSuiteProvider>( |
| identity_provider: &C, |
| cipher_suite_provider: &CSP, |
| path: UpdatePath, |
| state: &ProvisionalState, |
| sender: LeafIndex, |
| commit_time: Option<MlsTime>, |
| ) -> Result<ValidatedUpdatePath, MlsError> { |
| let group_context_extensions = &state.group_context.extensions; |
| |
| let leaf_validator = LeafNodeValidator::new( |
| cipher_suite_provider, |
| identity_provider, |
| Some(group_context_extensions), |
| ); |
| |
| leaf_validator |
| .check_if_valid( |
| &path.leaf_node, |
| ValidationContext::Commit((&state.group_context.group_id, *sender, commit_time)), |
| ) |
| .await?; |
| |
| let check_identity_eq = state.applied_proposals.external_initializations.is_empty(); |
| |
| if check_identity_eq { |
| let existing_leaf = state.public_tree.nodes.borrow_as_leaf(sender)?; |
| let original_leaf_node = existing_leaf.clone(); |
| |
| identity_provider |
| .valid_successor( |
| &original_leaf_node.signing_identity, |
| &path.leaf_node.signing_identity, |
| group_context_extensions, |
| ) |
| .await |
| .map_err(|e| MlsError::IdentityProviderError(e.into_any_error()))? |
| .then_some(()) |
| .ok_or(MlsError::InvalidSuccessor)?; |
| |
| (existing_leaf.public_key != path.leaf_node.public_key) |
| .then_some(()) |
| .ok_or(MlsError::SameHpkeKey(*sender))?; |
| } |
| |
| // Unfilter the update path |
| let filtered = state.public_tree.nodes.filtered(sender)?; |
| let mut unfiltered_nodes = vec![]; |
| let mut i = 0; |
| |
| for n in path.nodes { |
| while *filtered.get(i).ok_or(MlsError::WrongPathLen)? { |
| unfiltered_nodes.push(None); |
| i += 1; |
| } |
| |
| unfiltered_nodes.push(Some(n)); |
| i += 1; |
| } |
| |
| Ok(ValidatedUpdatePath { |
| leaf_node: path.leaf_node, |
| nodes: unfiltered_nodes, |
| }) |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use alloc::vec; |
| use assert_matches::assert_matches; |
| |
| use crate::client::test_utils::TEST_CIPHER_SUITE; |
| use crate::crypto::test_utils::test_cipher_suite_provider; |
| use crate::crypto::HpkeCiphertext; |
| use crate::group::message_processor::ProvisionalState; |
| use crate::group::test_utils::{get_test_group_context, random_bytes, TEST_GROUP}; |
| use crate::identity::basic::BasicIdentityProvider; |
| use crate::tree_kem::leaf_node::test_utils::default_properties; |
| use crate::tree_kem::leaf_node::test_utils::get_basic_test_node_sig_key; |
| use crate::tree_kem::leaf_node::LeafNodeSource; |
| use crate::tree_kem::node::LeafIndex; |
| use crate::tree_kem::parent_hash::ParentHash; |
| use crate::tree_kem::test_utils::{get_test_leaf_nodes, get_test_tree}; |
| use crate::tree_kem::validate_update_path; |
| |
| use super::{UpdatePath, UpdatePathNode}; |
| use crate::{cipher_suite::CipherSuite, tree_kem::MlsError}; |
| |
| use alloc::vec::Vec; |
| |
| #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] |
| async fn test_update_path(cipher_suite: CipherSuite, cred: &str) -> UpdatePath { |
| let (mut leaf_node, _, signer) = get_basic_test_node_sig_key(cipher_suite, cred).await; |
| |
| leaf_node.leaf_node_source = LeafNodeSource::Commit(ParentHash::from(hex!("beef"))); |
| |
| leaf_node |
| .commit( |
| &test_cipher_suite_provider(cipher_suite), |
| TEST_GROUP, |
| 0, |
| default_properties(), |
| None, |
| &signer, |
| ) |
| .await |
| .unwrap(); |
| |
| let node = UpdatePathNode { |
| public_key: random_bytes(32).into(), |
| encrypted_path_secret: vec![HpkeCiphertext { |
| kem_output: random_bytes(32), |
| ciphertext: random_bytes(32), |
| }], |
| }; |
| |
| UpdatePath { |
| leaf_node, |
| nodes: vec![node.clone(), node], |
| } |
| } |
| |
| #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] |
| async fn test_provisional_state(cipher_suite: CipherSuite) -> ProvisionalState { |
| let mut tree = get_test_tree(cipher_suite).await.public; |
| let leaf_nodes = get_test_leaf_nodes(cipher_suite).await; |
| |
| tree.add_leaves( |
| leaf_nodes, |
| &BasicIdentityProvider, |
| &test_cipher_suite_provider(cipher_suite), |
| ) |
| .await |
| .unwrap(); |
| |
| ProvisionalState { |
| public_tree: tree, |
| applied_proposals: Default::default(), |
| group_context: get_test_group_context(1, cipher_suite).await, |
| indexes_of_added_kpkgs: vec![], |
| external_init_index: None, |
| #[cfg(feature = "state_update")] |
| unused_proposals: vec![], |
| } |
| } |
| |
| #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] |
| async fn test_valid_leaf_node() { |
| let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE); |
| let update_path = test_update_path(TEST_CIPHER_SUITE, "creator").await; |
| |
| let validated = validate_update_path( |
| &BasicIdentityProvider, |
| &cipher_suite_provider, |
| update_path.clone(), |
| &test_provisional_state(TEST_CIPHER_SUITE).await, |
| LeafIndex(0), |
| None, |
| ) |
| .await |
| .unwrap(); |
| |
| let expected = update_path.nodes.into_iter().map(Some).collect::<Vec<_>>(); |
| |
| assert_eq!(validated.nodes, expected); |
| assert_eq!(validated.leaf_node, update_path.leaf_node); |
| } |
| |
| #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] |
| async fn test_invalid_key_package() { |
| let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE); |
| let mut update_path = test_update_path(TEST_CIPHER_SUITE, "creator").await; |
| update_path.leaf_node.signature = random_bytes(32); |
| |
| let validated = validate_update_path( |
| &BasicIdentityProvider, |
| &cipher_suite_provider, |
| update_path, |
| &test_provisional_state(TEST_CIPHER_SUITE).await, |
| LeafIndex(0), |
| None, |
| ) |
| .await; |
| |
| assert_matches!(validated, Err(MlsError::InvalidSignature)); |
| } |
| |
| #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] |
| async fn validating_path_fails_with_different_identity() { |
| let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE); |
| let cipher_suite = TEST_CIPHER_SUITE; |
| let update_path = test_update_path(cipher_suite, "foobar").await; |
| |
| let validated = validate_update_path( |
| &BasicIdentityProvider, |
| &cipher_suite_provider, |
| update_path, |
| &test_provisional_state(cipher_suite).await, |
| LeafIndex(0), |
| None, |
| ) |
| .await; |
| |
| assert_matches!(validated, Err(MlsError::InvalidSuccessor)); |
| } |
| |
| #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] |
| async fn validating_path_fails_with_same_hpke_key() { |
| let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE); |
| let update_path = test_update_path(TEST_CIPHER_SUITE, "creator").await; |
| let mut state = test_provisional_state(TEST_CIPHER_SUITE).await; |
| |
| state |
| .public_tree |
| .nodes |
| .borrow_as_leaf_mut(LeafIndex(0)) |
| .unwrap() |
| .public_key = update_path.leaf_node.public_key.clone(); |
| |
| let validated = validate_update_path( |
| &BasicIdentityProvider, |
| &cipher_suite_provider, |
| update_path, |
| &state, |
| LeafIndex(0), |
| None, |
| ) |
| .await; |
| |
| assert_matches!(validated, Err(MlsError::SameHpkeKey(_))); |
| } |
| } |