diff --git a/mls-rs/Cargo.toml b/mls-rs/Cargo.toml index 69ce1622..1b23c722 100644 --- a/mls-rs/Cargo.toml +++ b/mls-rs/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mls-rs" -version = "0.41.3" +version = "0.41.4" edition = "2021" description = "An implementation of Messaging Layer Security (RFC 9420)" homepage = "https://github.com/awslabs/mls-rs" diff --git a/mls-rs/src/client.rs b/mls-rs/src/client.rs index d21328e2..574d4ba1 100644 --- a/mls-rs/src/client.rs +++ b/mls-rs/src/client.rs @@ -7,13 +7,16 @@ use crate::client_builder::{recreate_config, BaseConfig, ClientBuilder, MakeConf use crate::client_config::ClientConfig; use crate::group::framing::MlsMessage; +use crate::group::{cipher_suite_provider, validate_group_info_joiner, GroupInfo}; +use crate::group::{ + framing::MlsMessagePayload, snapshot::Snapshot, ExportedTree, Group, NewMemberInfo, +}; #[cfg(feature = "by_ref_proposal")] use crate::group::{ - framing::{Content, MlsMessagePayload, PublicMessage, Sender, WireFormat}, + framing::{Content, PublicMessage, Sender, WireFormat}, message_signature::AuthenticatedContent, proposal::{AddProposal, Proposal}, }; -use crate::group::{snapshot::Snapshot, ExportedTree, Group, NewMemberInfo}; use crate::identity::SigningIdentity; use crate::key_package::{KeyPackageGeneration, KeyPackageGenerator}; use crate::protocol_version::ProtocolVersion; @@ -24,7 +27,7 @@ use mls_rs_core::crypto::{CryptoProvider, SignatureSecretKey}; use mls_rs_core::error::{AnyError, IntoAnyError}; use mls_rs_core::extension::{ExtensionError, ExtensionList, ExtensionType}; use mls_rs_core::group::{GroupStateStorage, ProposalType}; -use mls_rs_core::identity::CredentialType; +use mls_rs_core::identity::{CredentialType, IdentityProvider}; use mls_rs_core::key_package::KeyPackageStorage; use crate::group::external_commit::ExternalCommitBuilder; @@ -546,6 +549,46 @@ where .await } + /// Decrypt GroupInfo encrypted in the Welcome message without actually joining + /// the group. The ratchet tree is not needed. + #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] + pub async fn examine_welcome_message( + &self, + welcome_message: &MlsMessage, + ) -> Result { + Group::decrypt_group_info(welcome_message, &self.config).await + } + + /// Validate GroupInfo message. This does NOT validate the ratchet tree in case + /// it is provided in the extension. It validates the signature, identity of the + /// signer, identities of external senders and cipher suite. + #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] + pub async fn validate_group_info( + &self, + group_info_message: &MlsMessage, + signer: &SigningIdentity, + ) -> Result<(), MlsError> { + let MlsMessagePayload::GroupInfo(group_info) = &group_info_message.payload else { + return Err(MlsError::UnexpectedMessageType); + }; + + let cs = cipher_suite_provider( + self.config.crypto_provider(), + group_info.group_context.cipher_suite, + )?; + + let id = self.config.identity_provider(); + + validate_group_info_joiner(group_info_message.version, group_info, signer, &id, &cs) + .await?; + + id.validate_member(signer, None, Some(&group_info.group_context.extensions)) + .await + .map_err(|e| MlsError::IdentityProviderError(e.into_any_error()))?; + + Ok(()) + } + /// 0-RTT add to an existing [group](crate::group::Group) /// /// External commits allow for immediate entry into a @@ -650,7 +693,7 @@ where .cipher_suite_provider(cipher_suite) .ok_or(MlsError::UnsupportedCipherSuite(cipher_suite))?; - crate::group::validate_group_info_joiner( + crate::group::validate_tree_and_info_joiner( protocol_version, group_info, tree_data, @@ -1050,4 +1093,70 @@ mod tests { let bob = alice.to_builder().extension_type(34.into()).build(); assert_eq!(bob.config.supported_extensions(), [33, 34].map(Into::into)); } + + #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] + async fn examine_welcome_message() { + let mut alice = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE) + .await + .group; + + let (bob, kp) = + test_client_with_key_pkg(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "bob").await; + + let commit = alice + .commit_builder() + .add_member(kp) + .unwrap() + .build() + .await + .unwrap(); + + alice.apply_pending_commit().await.unwrap(); + + let mut group_info = bob + .examine_welcome_message(&commit.welcome_messages[0]) + .await + .unwrap(); + + // signature is random so we won't compare it + group_info.signature = vec![]; + group_info.ungrease(); + + let mut expected_group_info = alice + .group_info_message(commit.ratchet_tree.is_none()) + .await + .unwrap() + .into_group_info() + .unwrap(); + + expected_group_info.signature = vec![]; + expected_group_info.ungrease(); + + assert_eq!(expected_group_info, group_info); + } + + #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] + async fn validate_group_info() { + let alice = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE) + .await + .group; + + let bob = test_client_with_key_pkg(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE, "bob") + .await + .0; + + let group_info = alice.group_info_message(false).await.unwrap(); + let alice_signer = alice.current_member_signing_identity().unwrap().clone(); + + bob.validate_group_info(&group_info, &alice_signer) + .await + .unwrap(); + + let other_signer = get_test_signing_identity(TEST_CIPHER_SUITE, b"alice") + .await + .0; + + let res = bob.validate_group_info(&group_info, &other_signer).await; + assert_matches!(res, Err(MlsError::InvalidSignature)); + } } diff --git a/mls-rs/src/external_client/group.rs b/mls-rs/src/external_client/group.rs index 70c66f2b..7e150b09 100644 --- a/mls-rs/src/external_client/group.rs +++ b/mls-rs/src/external_client/group.rs @@ -26,7 +26,7 @@ use crate::{ snapshot::RawGroupState, state::GroupState, transcript_hash::InterimTranscriptHash, - validate_group_info_joiner, ContentType, ExportedTree, GroupContext, GroupInfo, Roster, + validate_tree_and_info_joiner, ContentType, ExportedTree, GroupContext, GroupInfo, Roster, Welcome, }, identity::SigningIdentity, @@ -129,7 +129,7 @@ impl ExternalGroup { group_info.group_context.cipher_suite, )?; - let public_tree = validate_group_info_joiner( + let public_tree = validate_tree_and_info_joiner( protocol_version, &group_info, tree_data, diff --git a/mls-rs/src/grease.rs b/mls-rs/src/grease.rs index cd4f208e..3d16c219 100644 --- a/mls-rs/src/grease.rs +++ b/mls-rs/src/grease.rs @@ -55,6 +55,10 @@ impl GroupInfo { pub fn grease(&mut self, cs: &P) -> Result<(), MlsError> { grease_functions::grease_extensions(&mut self.extensions, cs).map(|_| ()) } + + pub fn ungrease(&mut self) { + grease_functions::ungrease_extensions(&mut self.extensions) + } } impl NewMemberInfo { diff --git a/mls-rs/src/group/external_commit.rs b/mls-rs/src/group/external_commit.rs index fe5dfd76..0c931a09 100644 --- a/mls-rs/src/group/external_commit.rs +++ b/mls-rs/src/group/external_commit.rs @@ -39,7 +39,7 @@ use crate::group::{ PreSharedKeyProposal, {JustPreSharedKeyID, PreSharedKeyID}, }; -use super::{validate_group_info_joiner, ExportedTree}; +use super::{validate_tree_and_info_joiner, ExportedTree}; /// A builder that aids with the construction of an external commit. #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::ffi_type(opaque))] @@ -163,7 +163,7 @@ impl ExternalCommitBuilder { .get_as::()? .ok_or(MlsError::MissingExternalPubExtension)?; - let public_tree = validate_group_info_joiner( + let public_tree = validate_tree_and_info_joiner( protocol_version, &group_info, self.tree_data, diff --git a/mls-rs/src/group/mod.rs b/mls-rs/src/group/mod.rs index b90b5b00..ccfdd923 100644 --- a/mls-rs/src/group/mod.rs +++ b/mls-rs/src/group/mod.rs @@ -20,7 +20,7 @@ use crate::crypto::{HpkeCiphertext, SignatureSecretKey}; use crate::extension::LastResortKeyPackageExt; use crate::extension::RatchetTreeExt; use crate::identity::SigningIdentity; -use crate::key_package::{KeyPackage, KeyPackageRef}; +use crate::key_package::{KeyPackage, KeyPackageGeneration, KeyPackageRef}; use crate::protocol_version::ProtocolVersion; use crate::psk::secret::PskSecret; use crate::psk::PreSharedKeyID; @@ -419,102 +419,33 @@ where signer: SignatureSecretKey, #[cfg(feature = "psk")] additional_psk: Option, ) -> Result<(Self, NewMemberInfo), MlsError> { - let protocol_version = welcome.version; - - if !config.version_supported(protocol_version) { - return Err(MlsError::UnsupportedProtocolVersion(protocol_version)); - } - - let MlsMessagePayload::Welcome(welcome) = &welcome.payload else { - return Err(MlsError::UnexpectedMessageType); - }; - - let cipher_suite_provider = - cipher_suite_provider(config.crypto_provider(), welcome.cipher_suite)?; - - let (encrypted_group_secrets, key_package_generation) = - find_key_package_generation(&config.key_package_repo(), &welcome.secrets).await?; - - let key_package = &key_package_generation.key_package; - - if key_package.version != protocol_version { - return Err(MlsError::ProtocolVersionMismatch); - } - - // Decrypt the encrypted_group_secrets using HPKE with the algorithms indicated by the - // cipher suite and the HPKE private key corresponding to the GroupSecrets. If a - // PreSharedKeyID is part of the GroupSecrets and the client is not in possession of - // the corresponding PSK, return an error - let group_secrets = GroupSecrets::decrypt( - &cipher_suite_provider, - &key_package_generation.init_secret_key, - &key_package.hpke_init_key, - &welcome.encrypted_group_info, - &encrypted_group_secrets.encrypted_group_secrets, - ) - .await?; - - #[cfg(feature = "psk")] - let psk_secret = if let Some(psk) = additional_psk { - let psk_id = group_secrets - .psks - .first() - .ok_or(MlsError::UnexpectedPskId)?; - - match &psk_id.key_id { - JustPreSharedKeyID::Resumption(r) if r.usage != ResumptionPSKUsage::Application => { - Ok(()) - } - _ => Err(MlsError::UnexpectedPskId), - }?; - - let mut psk = psk; - psk.id.psk_nonce = psk_id.psk_nonce.clone(); - PskSecret::calculate(&[psk], &cipher_suite_provider).await? - } else { - PskResolver::< - ::GroupStateStorage, - ::KeyPackageRepository, - ::PskStore, - > { - group_context: None, - current_epoch: None, - prior_epochs: None, - psk_store: &config.secret_store(), - } - .resolve_to_secret(&group_secrets.psks, &cipher_suite_provider) - .await? - }; - - #[cfg(not(feature = "psk"))] - let psk_secret = PskSecret::new(&cipher_suite_provider); - - // From the joiner_secret in the decrypted GroupSecrets object and the PSKs specified in - // the GroupSecrets, derive the welcome_secret and using that the welcome_key and - // welcome_nonce. - let welcome_secret = WelcomeSecret::from_joiner_secret( - &cipher_suite_provider, - &group_secrets.joiner_secret, - &psk_secret, - ) - .await?; - - // Use the key and nonce to decrypt the encrypted_group_info field. - let decrypted_group_info = welcome_secret - .decrypt(&welcome.encrypted_group_info) + let (group_info, key_package_generation, group_secrets, psk_secret) = + Self::decrypt_group_info_internal( + welcome, + &config, + #[cfg(feature = "psk")] + additional_psk, + ) .await?; - let group_info = GroupInfo::mls_decode(&mut &**decrypted_group_info)?; + let cipher_suite_provider = cipher_suite_provider( + config.crypto_provider(), + group_info.group_context.cipher_suite, + )?; - let public_tree = validate_group_info_joiner( - protocol_version, + let id_provider = config.identity_provider(); + + let public_tree = validate_tree_and_info_joiner( + welcome.version, &group_info, tree_data, - &config.identity_provider(), + &id_provider, &cipher_suite_provider, ) .await?; + let key_package = key_package_generation.key_package; + // Identify a leaf in the tree array (any even-numbered node) whose leaf_node is identical // to the leaf_node field of the KeyPackage. If no such field exists, return an error. Let // index represent the index of this node among the leaves in the tree, namely the index of @@ -1617,6 +1548,145 @@ where } } +impl Group { + #[cfg(feature = "psk")] + #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] + async fn psk_secret( + config: &C, + cipher_suite_provider: &CS, + psks: &[PreSharedKeyID], + additional_psk: Option, + ) -> Result { + if let Some(psk) = additional_psk { + let psk_id = psks.first().ok_or(MlsError::UnexpectedPskId)?; + + match &psk_id.key_id { + JustPreSharedKeyID::Resumption(r) if r.usage != ResumptionPSKUsage::Application => { + Ok(()) + } + _ => Err(MlsError::UnexpectedPskId), + }?; + + let mut psk = psk; + psk.id.psk_nonce = psk_id.psk_nonce.clone(); + PskSecret::calculate(&[psk], cipher_suite_provider).await + } else { + PskResolver::< + ::GroupStateStorage, + ::KeyPackageRepository, + ::PskStore, + > { + group_context: None, + current_epoch: None, + prior_epochs: None, + psk_store: &config.secret_store(), + } + .resolve_to_secret(psks, cipher_suite_provider) + .await + } + } + + #[cfg(not(feature = "psk"))] + #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] + async fn psk_secret( + _config: &C, + cipher_suite_provider: &CS, + _psks: &[PreSharedKeyID], + ) -> Result { + Ok(PskSecret::new(cipher_suite_provider)) + } + + #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] + pub(crate) async fn decrypt_group_info( + welcome: &MlsMessage, + config: &C, + ) -> Result { + Self::decrypt_group_info_internal( + welcome, + config, + #[cfg(feature = "psk")] + None, + ) + .await + .map(|info| info.0) + } + + #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] + async fn decrypt_group_info_internal( + welcome: &MlsMessage, + config: &C, + #[cfg(feature = "psk")] additional_psk: Option, + ) -> Result<(GroupInfo, KeyPackageGeneration, GroupSecrets, PskSecret), MlsError> { + let protocol_version = welcome.version; + + if !config.version_supported(protocol_version) { + return Err(MlsError::UnsupportedProtocolVersion(protocol_version)); + } + + let MlsMessagePayload::Welcome(welcome) = &welcome.payload else { + return Err(MlsError::UnexpectedMessageType); + }; + + let cipher_suite_provider = + cipher_suite_provider(config.crypto_provider(), welcome.cipher_suite)?; + + let (encrypted_group_secrets, key_package_generation) = + find_key_package_generation(&config.key_package_repo(), &welcome.secrets).await?; + + let key_package_version = key_package_generation.key_package.version; + + if key_package_version != protocol_version { + return Err(MlsError::ProtocolVersionMismatch); + } + + // Decrypt the encrypted_group_secrets using HPKE with the algorithms indicated by the + // cipher suite and the HPKE private key corresponding to the GroupSecrets. If a + // PreSharedKeyID is part of the GroupSecrets and the client is not in possession of + // the corresponding PSK, return an error + let group_secrets = GroupSecrets::decrypt( + &cipher_suite_provider, + &key_package_generation.init_secret_key, + &key_package_generation.key_package.hpke_init_key, + &welcome.encrypted_group_info, + &encrypted_group_secrets.encrypted_group_secrets, + ) + .await?; + + let psk_secret = Self::psk_secret( + config, + &cipher_suite_provider, + &group_secrets.psks, + #[cfg(feature = "psk")] + additional_psk, + ) + .await?; + + // From the joiner_secret in the decrypted GroupSecrets object and the PSKs specified in + // the GroupSecrets, derive the welcome_secret and using that the welcome_key and + // welcome_nonce. + let welcome_secret = WelcomeSecret::from_joiner_secret( + &cipher_suite_provider, + &group_secrets.joiner_secret, + &psk_secret, + ) + .await?; + + // Use the key and nonce to decrypt the encrypted_group_info field. + let decrypted_group_info = welcome_secret + .decrypt(&welcome.encrypted_group_info) + .await?; + + let group_info = GroupInfo::mls_decode(&mut &**decrypted_group_info)?; + + Ok(( + group_info, + key_package_generation, + group_secrets, + psk_secret, + )) + } +} + #[cfg(feature = "private_message")] impl GroupStateProvider for Group where diff --git a/mls-rs/src/group/util.rs b/mls-rs/src/group/util.rs index dadfafac..67b91d51 100644 --- a/mls-rs/src/group/util.rs +++ b/mls-rs/src/group/util.rs @@ -3,7 +3,9 @@ // SPDX-License-Identifier: (Apache-2.0 OR MIT) use mls_rs_core::{ - error::IntoAnyError, identity::IdentityProvider, key_package::KeyPackageStorage, + error::IntoAnyError, + identity::{IdentityProvider, SigningIdentity}, + key_package::KeyPackageStorage, }; use crate::{ @@ -32,7 +34,7 @@ use super::message_processor::ProvisionalState; pub(crate) async fn validate_group_info_common( msg_version: ProtocolVersion, group_info: &GroupInfo, - tree: &TreeKemPublic, + signer: &SigningIdentity, cs: &C, ) -> Result<(), MlsError> { if msg_version != group_info.group_context.protocol_version { @@ -43,11 +45,7 @@ pub(crate) async fn validate_group_info_common( return Err(MlsError::CipherSuiteMismatch); } - let sender_leaf = &tree.get_leaf_node(group_info.signer)?; - - group_info - .verify(cs, &sender_leaf.signing_identity.signature_key, &()) - .await?; + group_info.verify(cs, &signer.signature_key, &()).await?; Ok(()) } @@ -59,7 +57,8 @@ pub(crate) async fn validate_group_info_member( group_info: &GroupInfo, cs: &C, ) -> Result<(), MlsError> { - validate_group_info_common(msg_version, group_info, &self_state.public_tree, cs).await?; + let signer = &self_state.public_tree.get_leaf_node(group_info.signer)?; + validate_group_info_common(msg_version, group_info, &signer.signing_identity, cs).await?; let self_tree = ExportedTree::new_borrowed(&self_state.public_tree.nodes); @@ -78,17 +77,31 @@ pub(crate) async fn validate_group_info_member( } #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] -pub(crate) async fn validate_group_info_joiner( +pub(crate) async fn validate_tree_and_info_joiner( msg_version: ProtocolVersion, group_info: &GroupInfo, tree: Option>, id_provider: &I, cs: &C, -) -> Result -where - C: CipherSuiteProvider, - I: IdentityProvider, -{ +) -> Result { + let public_tree = validate_tree_joiner(group_info, tree, id_provider, cs).await?; + + let signer = &public_tree + .get_leaf_node(group_info.signer)? + .signing_identity; + + validate_group_info_joiner(msg_version, group_info, signer, id_provider, cs).await?; + + Ok(public_tree) +} + +#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] +pub(crate) async fn validate_tree_joiner( + group_info: &GroupInfo, + tree: Option>, + id_provider: &I, + cs: &C, +) -> Result { let tree = match group_info.extensions.get_as::()? { Some(ext) => ext.tree_data, None => tree.ok_or(MlsError::RatchetTreeNotFound)?, @@ -104,6 +117,21 @@ where .validate(&mut tree) .await?; + Ok(tree) +} + +#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] +pub(crate) async fn validate_group_info_joiner( + msg_version: ProtocolVersion, + group_info: &GroupInfo, + signer: &SigningIdentity, + #[cfg(feature = "by_ref_proposal")] id_provider: &I, + #[cfg(not(feature = "by_ref_proposal"))] _id_provider: &I, + cs: &C, +) -> Result<(), MlsError> { + #[cfg(feature = "by_ref_proposal")] + let context = &group_info.group_context; + #[cfg(feature = "by_ref_proposal")] if let Some(ext_senders) = context.extensions.get_as::()? { // TODO do joiners verify group against current time?? @@ -113,9 +141,9 @@ where .map_err(|e| MlsError::IdentityProviderError(e.into_any_error()))?; } - validate_group_info_common(msg_version, group_info, &tree, cs).await?; + validate_group_info_common(msg_version, group_info, signer, cs).await?; - Ok(tree) + Ok(()) } pub(crate) fn commit_sender(