| // 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 super::{parent_hash::ParentHash, Capabilities, Lifetime}; |
| use crate::client::MlsError; |
| use crate::crypto::{CipherSuiteProvider, HpkePublicKey, HpkeSecretKey, SignatureSecretKey}; |
| use crate::{identity::SigningIdentity, signer::Signable, ExtensionList}; |
| use alloc::vec::Vec; |
| use core::fmt::{self, Debug}; |
| use mls_rs_codec::{MlsDecode, MlsEncode, MlsSize}; |
| use mls_rs_core::error::IntoAnyError; |
| |
| #[derive(Debug, Clone, MlsSize, MlsEncode, MlsDecode, PartialEq, Eq)] |
| #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] |
| #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
| #[repr(u8)] |
| pub enum LeafNodeSource { |
| KeyPackage(Lifetime) = 1u8, |
| Update = 2u8, |
| Commit(ParentHash) = 3u8, |
| } |
| |
| #[derive(Clone, MlsSize, MlsEncode, MlsDecode, PartialEq)] |
| #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] |
| #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
| #[non_exhaustive] |
| pub struct LeafNode { |
| pub public_key: HpkePublicKey, |
| pub signing_identity: SigningIdentity, |
| pub capabilities: Capabilities, |
| pub leaf_node_source: LeafNodeSource, |
| pub extensions: ExtensionList, |
| #[mls_codec(with = "mls_rs_codec::byte_vec")] |
| #[cfg_attr(feature = "serde", serde(with = "mls_rs_core::vec_serde"))] |
| pub signature: Vec<u8>, |
| } |
| |
| impl Debug for LeafNode { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| f.debug_struct("LeafNode") |
| .field("public_key", &self.public_key) |
| .field("signing_identity", &self.signing_identity) |
| .field("capabilities", &self.capabilities) |
| .field("leaf_node_source", &self.leaf_node_source) |
| .field("extensions", &self.extensions) |
| .field( |
| "signature", |
| &mls_rs_core::debug::pretty_bytes(&self.signature), |
| ) |
| .finish() |
| } |
| } |
| |
| #[derive(Clone, Debug)] |
| pub struct ConfigProperties { |
| pub capabilities: Capabilities, |
| pub extensions: ExtensionList, |
| } |
| |
| impl LeafNode { |
| #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] |
| pub async fn generate<CSP>( |
| cipher_suite_provider: &CSP, |
| properties: ConfigProperties, |
| signing_identity: SigningIdentity, |
| signer: &SignatureSecretKey, |
| lifetime: Lifetime, |
| ) -> Result<(Self, HpkeSecretKey), MlsError> |
| where |
| CSP: CipherSuiteProvider, |
| { |
| let (secret_key, public_key) = cipher_suite_provider |
| .kem_generate() |
| .await |
| .map_err(|e| MlsError::CryptoProviderError(e.into_any_error()))?; |
| |
| let mut leaf_node = LeafNode { |
| public_key, |
| signing_identity, |
| capabilities: properties.capabilities, |
| leaf_node_source: LeafNodeSource::KeyPackage(lifetime), |
| extensions: properties.extensions, |
| signature: Default::default(), |
| }; |
| |
| leaf_node.grease(cipher_suite_provider)?; |
| |
| leaf_node |
| .sign( |
| cipher_suite_provider, |
| signer, |
| &LeafNodeSigningContext::default(), |
| ) |
| .await?; |
| |
| Ok((leaf_node, secret_key)) |
| } |
| |
| #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] |
| pub async fn update<P: CipherSuiteProvider>( |
| &mut self, |
| cipher_suite_provider: &P, |
| group_id: &[u8], |
| leaf_index: u32, |
| new_properties: ConfigProperties, |
| signing_identity: Option<SigningIdentity>, |
| signer: &SignatureSecretKey, |
| ) -> Result<HpkeSecretKey, MlsError> { |
| let (secret, public) = cipher_suite_provider |
| .kem_generate() |
| .await |
| .map_err(|e| MlsError::CryptoProviderError(e.into_any_error()))?; |
| |
| self.public_key = public; |
| self.capabilities = new_properties.capabilities; |
| self.extensions = new_properties.extensions; |
| self.leaf_node_source = LeafNodeSource::Update; |
| |
| self.grease(cipher_suite_provider)?; |
| |
| if let Some(signing_identity) = signing_identity { |
| self.signing_identity = signing_identity; |
| } |
| |
| self.sign( |
| cipher_suite_provider, |
| signer, |
| &(group_id, leaf_index).into(), |
| ) |
| .await?; |
| |
| Ok(secret) |
| } |
| |
| #[allow(clippy::too_many_arguments)] |
| #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] |
| pub async fn commit<P: CipherSuiteProvider>( |
| &mut self, |
| cipher_suite_provider: &P, |
| group_id: &[u8], |
| leaf_index: u32, |
| new_properties: ConfigProperties, |
| new_signing_identity: Option<SigningIdentity>, |
| signer: &SignatureSecretKey, |
| ) -> Result<HpkeSecretKey, MlsError> { |
| let (secret, public) = cipher_suite_provider |
| .kem_generate() |
| .await |
| .map_err(|e| MlsError::CryptoProviderError(e.into_any_error()))?; |
| |
| self.public_key = public; |
| self.capabilities = new_properties.capabilities; |
| self.extensions = new_properties.extensions; |
| |
| if let Some(new_signing_identity) = new_signing_identity { |
| self.signing_identity = new_signing_identity; |
| } |
| |
| self.sign( |
| cipher_suite_provider, |
| signer, |
| &(group_id, leaf_index).into(), |
| ) |
| .await?; |
| |
| Ok(secret) |
| } |
| } |
| |
| #[derive(Debug)] |
| struct LeafNodeTBS<'a> { |
| public_key: &'a HpkePublicKey, |
| signing_identity: &'a SigningIdentity, |
| capabilities: &'a Capabilities, |
| leaf_node_source: &'a LeafNodeSource, |
| extensions: &'a ExtensionList, |
| group_id: Option<&'a [u8]>, |
| leaf_index: Option<u32>, |
| } |
| |
| impl<'a> MlsSize for LeafNodeTBS<'a> { |
| fn mls_encoded_len(&self) -> usize { |
| self.public_key.mls_encoded_len() |
| + self.signing_identity.mls_encoded_len() |
| + self.capabilities.mls_encoded_len() |
| + self.leaf_node_source.mls_encoded_len() |
| + self.extensions.mls_encoded_len() |
| + self |
| .group_id |
| .as_ref() |
| .map_or(0, mls_rs_codec::byte_vec::mls_encoded_len) |
| + self.leaf_index.map_or(0, |i| i.mls_encoded_len()) |
| } |
| } |
| |
| impl<'a> MlsEncode for LeafNodeTBS<'a> { |
| fn mls_encode(&self, writer: &mut Vec<u8>) -> Result<(), mls_rs_codec::Error> { |
| self.public_key.mls_encode(writer)?; |
| self.signing_identity.mls_encode(writer)?; |
| self.capabilities.mls_encode(writer)?; |
| self.leaf_node_source.mls_encode(writer)?; |
| self.extensions.mls_encode(writer)?; |
| |
| if let Some(ref group_id) = self.group_id { |
| mls_rs_codec::byte_vec::mls_encode(group_id, writer)?; |
| } |
| |
| if let Some(leaf_index) = self.leaf_index { |
| leaf_index.mls_encode(writer)?; |
| } |
| |
| Ok(()) |
| } |
| } |
| |
| #[derive(Clone, Debug, Default)] |
| pub(crate) struct LeafNodeSigningContext<'a> { |
| pub group_id: Option<&'a [u8]>, |
| pub leaf_index: Option<u32>, |
| } |
| |
| impl<'a> From<(&'a [u8], u32)> for LeafNodeSigningContext<'a> { |
| fn from((group_id, leaf_index): (&'a [u8], u32)) -> Self { |
| Self { |
| group_id: Some(group_id), |
| leaf_index: Some(leaf_index), |
| } |
| } |
| } |
| |
| impl<'a> Signable<'a> for LeafNode { |
| const SIGN_LABEL: &'static str = "LeafNodeTBS"; |
| |
| type SigningContext = LeafNodeSigningContext<'a>; |
| |
| fn signature(&self) -> &[u8] { |
| &self.signature |
| } |
| |
| fn signable_content( |
| &self, |
| context: &Self::SigningContext, |
| ) -> Result<Vec<u8>, mls_rs_codec::Error> { |
| LeafNodeTBS { |
| public_key: &self.public_key, |
| signing_identity: &self.signing_identity, |
| capabilities: &self.capabilities, |
| leaf_node_source: &self.leaf_node_source, |
| extensions: &self.extensions, |
| group_id: context.group_id, |
| leaf_index: context.leaf_index, |
| } |
| .mls_encode_to_vec() |
| } |
| |
| fn write_signature(&mut self, signature: Vec<u8>) { |
| self.signature = signature |
| } |
| } |
| |
| #[cfg(test)] |
| pub(crate) mod test_utils { |
| use alloc::vec; |
| use mls_rs_core::identity::{BasicCredential, CredentialType}; |
| |
| use crate::{ |
| cipher_suite::CipherSuite, |
| crypto::test_utils::{test_cipher_suite_provider, TestCryptoProvider}, |
| identity::test_utils::{get_test_signing_identity, BasicWithCustomProvider}, |
| }; |
| |
| use crate::extension::ApplicationIdExt; |
| |
| use super::*; |
| |
| #[allow(unused)] |
| #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] |
| pub async fn get_test_node( |
| cipher_suite: CipherSuite, |
| signing_identity: SigningIdentity, |
| secret: &SignatureSecretKey, |
| capabilities: Option<Capabilities>, |
| extensions: Option<ExtensionList>, |
| ) -> (LeafNode, HpkeSecretKey) { |
| get_test_node_with_lifetime( |
| cipher_suite, |
| signing_identity, |
| secret, |
| capabilities.unwrap_or_else(get_test_capabilities), |
| extensions.unwrap_or_default(), |
| Lifetime::years(1).unwrap(), |
| ) |
| .await |
| } |
| |
| #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] |
| pub async fn get_test_node_with_lifetime( |
| cipher_suite: CipherSuite, |
| signing_identity: SigningIdentity, |
| secret: &SignatureSecretKey, |
| capabilities: Capabilities, |
| extensions: ExtensionList, |
| lifetime: Lifetime, |
| ) -> (LeafNode, HpkeSecretKey) { |
| let properties = ConfigProperties { |
| capabilities, |
| extensions, |
| }; |
| |
| LeafNode::generate( |
| &test_cipher_suite_provider(cipher_suite), |
| properties, |
| signing_identity, |
| secret, |
| lifetime, |
| ) |
| .await |
| .unwrap() |
| } |
| |
| #[allow(unused)] |
| #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] |
| pub async fn get_basic_test_node(cipher_suite: CipherSuite, id: &str) -> LeafNode { |
| get_basic_test_node_sig_key(cipher_suite, id).await.0 |
| } |
| |
| #[allow(unused)] |
| pub fn default_properties() -> ConfigProperties { |
| ConfigProperties { |
| capabilities: get_test_capabilities(), |
| extensions: Default::default(), |
| } |
| } |
| |
| #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] |
| pub async fn get_basic_test_node_capabilities( |
| cipher_suite: CipherSuite, |
| id: &str, |
| capabilities: Capabilities, |
| ) -> (LeafNode, HpkeSecretKey, SignatureSecretKey) { |
| let (signing_identity, signature_key) = |
| get_test_signing_identity(cipher_suite, id.as_bytes()).await; |
| |
| LeafNode::generate( |
| &test_cipher_suite_provider(cipher_suite), |
| ConfigProperties { |
| capabilities, |
| extensions: Default::default(), |
| }, |
| signing_identity, |
| &signature_key, |
| Lifetime::years(1).unwrap(), |
| ) |
| .await |
| .map(|(leaf, hpke_secret_key)| (leaf, hpke_secret_key, signature_key)) |
| .unwrap() |
| } |
| |
| #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] |
| pub async fn get_basic_test_node_sig_key( |
| cipher_suite: CipherSuite, |
| id: &str, |
| ) -> (LeafNode, HpkeSecretKey, SignatureSecretKey) { |
| get_basic_test_node_capabilities(cipher_suite, id, get_test_capabilities()).await |
| } |
| |
| #[allow(unused)] |
| pub fn get_test_extensions() -> ExtensionList { |
| let mut extension_list = ExtensionList::new(); |
| |
| extension_list |
| .set_from(ApplicationIdExt { |
| identifier: b"identifier".to_vec(), |
| }) |
| .unwrap(); |
| |
| extension_list |
| } |
| |
| pub fn get_test_capabilities() -> Capabilities { |
| Capabilities { |
| credentials: vec![ |
| BasicCredential::credential_type(), |
| CredentialType::from(BasicWithCustomProvider::CUSTOM_CREDENTIAL_TYPE), |
| ], |
| cipher_suites: TestCryptoProvider::all_supported_cipher_suites(), |
| ..Default::default() |
| } |
| } |
| |
| #[allow(unused)] |
| pub fn get_test_client_identity(leaf: &LeafNode) -> Vec<u8> { |
| leaf.signing_identity |
| .credential |
| .mls_encode_to_vec() |
| .unwrap() |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::test_utils::*; |
| use super::*; |
| |
| use crate::client::test_utils::TEST_CIPHER_SUITE; |
| use crate::crypto::test_utils::test_cipher_suite_provider; |
| use crate::crypto::test_utils::TestCryptoProvider; |
| use crate::group::test_utils::random_bytes; |
| use crate::identity::test_utils::get_test_signing_identity; |
| use assert_matches::assert_matches; |
| |
| #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] |
| async fn test_node_generation() { |
| let capabilities = get_test_capabilities(); |
| let extensions = get_test_extensions(); |
| let lifetime = Lifetime::years(1).unwrap(); |
| |
| for cipher_suite in TestCryptoProvider::all_supported_cipher_suites() { |
| let (signing_identity, secret) = get_test_signing_identity(cipher_suite, b"foo").await; |
| |
| let (leaf_node, secret_key) = get_test_node_with_lifetime( |
| cipher_suite, |
| signing_identity.clone(), |
| &secret, |
| capabilities.clone(), |
| extensions.clone(), |
| lifetime.clone(), |
| ) |
| .await; |
| |
| assert_eq!(leaf_node.ungreased_capabilities(), capabilities); |
| assert_eq!(leaf_node.ungreased_extensions(), extensions); |
| assert_eq!(leaf_node.signing_identity, signing_identity); |
| |
| assert_matches!( |
| &leaf_node.leaf_node_source, |
| LeafNodeSource::KeyPackage(lt) if lt == &lifetime, |
| "Expected {:?}, got {:?}", LeafNodeSource::KeyPackage(lifetime), |
| leaf_node.leaf_node_source |
| ); |
| |
| let provider = test_cipher_suite_provider(cipher_suite); |
| |
| // Verify that the hpke key pair generated will work |
| let test_data = random_bytes(32); |
| |
| let sealed = provider |
| .hpke_seal(&leaf_node.public_key, &[], None, &test_data) |
| .await |
| .unwrap(); |
| |
| let opened = provider |
| .hpke_open(&sealed, &secret_key, &leaf_node.public_key, &[], None) |
| .await |
| .unwrap(); |
| |
| assert_eq!(opened, test_data); |
| |
| leaf_node |
| .verify( |
| &test_cipher_suite_provider(cipher_suite), |
| &signing_identity.signature_key, |
| &LeafNodeSigningContext::default(), |
| ) |
| .await |
| .unwrap(); |
| } |
| } |
| |
| #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] |
| async fn test_node_generation_randomness() { |
| let cipher_suite = TEST_CIPHER_SUITE; |
| |
| let (signing_identity, secret) = get_test_signing_identity(cipher_suite, b"foo").await; |
| |
| let (first_leaf, first_secret) = |
| get_test_node(cipher_suite, signing_identity.clone(), &secret, None, None).await; |
| |
| for _ in 0..100 { |
| let (next_leaf, next_secret) = |
| get_test_node(cipher_suite, signing_identity.clone(), &secret, None, None).await; |
| |
| assert_ne!(first_secret, next_secret); |
| assert_ne!(first_leaf.public_key, next_leaf.public_key); |
| } |
| } |
| |
| #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] |
| async fn test_node_update_no_meta_changes() { |
| for cipher_suite in TestCryptoProvider::all_supported_cipher_suites() { |
| let cipher_suite_provider = test_cipher_suite_provider(cipher_suite); |
| |
| let (signing_identity, secret) = get_test_signing_identity(cipher_suite, b"foo").await; |
| |
| let (mut leaf, leaf_secret) = |
| get_test_node(cipher_suite, signing_identity.clone(), &secret, None, None).await; |
| |
| let original_leaf = leaf.clone(); |
| |
| let new_secret = leaf |
| .update( |
| &cipher_suite_provider, |
| b"group", |
| 0, |
| default_properties(), |
| None, |
| &secret, |
| ) |
| .await |
| .unwrap(); |
| |
| assert_ne!(new_secret, leaf_secret); |
| assert_ne!(original_leaf.public_key, leaf.public_key); |
| |
| assert_eq!( |
| leaf.ungreased_capabilities(), |
| original_leaf.ungreased_capabilities() |
| ); |
| |
| assert_eq!( |
| leaf.ungreased_extensions(), |
| original_leaf.ungreased_extensions() |
| ); |
| |
| assert_eq!(leaf.signing_identity, original_leaf.signing_identity); |
| assert_matches!(&leaf.leaf_node_source, LeafNodeSource::Update); |
| |
| leaf.verify( |
| &cipher_suite_provider, |
| &signing_identity.signature_key, |
| &(b"group".as_slice(), 0).into(), |
| ) |
| .await |
| .unwrap(); |
| } |
| } |
| |
| #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] |
| async fn test_node_update_meta_changes() { |
| let cipher_suite = TEST_CIPHER_SUITE; |
| |
| let (signing_identity, secret) = get_test_signing_identity(cipher_suite, b"foo").await; |
| |
| let new_properties = ConfigProperties { |
| capabilities: get_test_capabilities(), |
| extensions: get_test_extensions(), |
| }; |
| |
| let (mut leaf, _) = |
| get_test_node(cipher_suite, signing_identity, &secret, None, None).await; |
| |
| leaf.update( |
| &test_cipher_suite_provider(cipher_suite), |
| b"group", |
| 0, |
| new_properties.clone(), |
| None, |
| &secret, |
| ) |
| .await |
| .unwrap(); |
| |
| assert_eq!(leaf.ungreased_capabilities(), new_properties.capabilities); |
| assert_eq!(leaf.ungreased_extensions(), new_properties.extensions); |
| } |
| |
| #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] |
| async fn test_node_commit_no_meta_changes() { |
| for cipher_suite in TestCryptoProvider::all_supported_cipher_suites() { |
| let cipher_suite_provider = test_cipher_suite_provider(cipher_suite); |
| |
| let (signing_identity, secret) = get_test_signing_identity(cipher_suite, b"foo").await; |
| |
| let (mut leaf, leaf_secret) = |
| get_test_node(cipher_suite, signing_identity.clone(), &secret, None, None).await; |
| |
| let original_leaf = leaf.clone(); |
| |
| let new_secret = leaf |
| .commit( |
| &cipher_suite_provider, |
| b"group", |
| 0, |
| default_properties(), |
| None, |
| &secret, |
| ) |
| .await |
| .unwrap(); |
| |
| assert_ne!(new_secret, leaf_secret); |
| assert_ne!(original_leaf.public_key, leaf.public_key); |
| |
| assert_eq!( |
| leaf.ungreased_capabilities(), |
| original_leaf.ungreased_capabilities() |
| ); |
| |
| assert_eq!( |
| leaf.ungreased_extensions(), |
| original_leaf.ungreased_extensions() |
| ); |
| |
| assert_eq!(leaf.signing_identity, original_leaf.signing_identity); |
| |
| leaf.verify( |
| &cipher_suite_provider, |
| &signing_identity.signature_key, |
| &(b"group".as_slice(), 0).into(), |
| ) |
| .await |
| .unwrap(); |
| } |
| } |
| |
| #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] |
| async fn test_node_commit_meta_changes() { |
| let cipher_suite = TEST_CIPHER_SUITE; |
| |
| let (signing_identity, secret) = get_test_signing_identity(cipher_suite, b"foo").await; |
| let (mut leaf, _) = |
| get_test_node(cipher_suite, signing_identity, &secret, None, None).await; |
| |
| let new_properties = ConfigProperties { |
| capabilities: get_test_capabilities(), |
| extensions: get_test_extensions(), |
| }; |
| |
| // The new identity has a fresh public key |
| let new_signing_identity = get_test_signing_identity(cipher_suite, b"foo").await.0; |
| |
| leaf.commit( |
| &test_cipher_suite_provider(cipher_suite), |
| b"group", |
| 0, |
| new_properties.clone(), |
| Some(new_signing_identity.clone()), |
| &secret, |
| ) |
| .await |
| .unwrap(); |
| |
| assert_eq!(leaf.capabilities, new_properties.capabilities); |
| assert_eq!(leaf.extensions, new_properties.extensions); |
| assert_eq!(leaf.signing_identity, new_signing_identity); |
| } |
| |
| #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] |
| async fn context_is_signed() { |
| let provider = test_cipher_suite_provider(TEST_CIPHER_SUITE); |
| |
| let (signing_identity, secret) = get_test_signing_identity(TEST_CIPHER_SUITE, b"foo").await; |
| |
| let (mut leaf, _) = get_test_node( |
| TEST_CIPHER_SUITE, |
| signing_identity.clone(), |
| &secret, |
| None, |
| None, |
| ) |
| .await; |
| |
| leaf.sign(&provider, &secret, &(b"foo".as_slice(), 0).into()) |
| .await |
| .unwrap(); |
| |
| let res = leaf |
| .verify( |
| &provider, |
| &signing_identity.signature_key, |
| &(b"foo".as_slice(), 1).into(), |
| ) |
| .await; |
| |
| assert_matches!(res, Err(MlsError::InvalidSignature)); |
| |
| let res = leaf |
| .verify( |
| &provider, |
| &signing_identity.signature_key, |
| &(b"bar".as_slice(), 0).into(), |
| ) |
| .await; |
| |
| assert_matches!(res, Err(MlsError::InvalidSignature)); |
| } |
| } |