| // 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 mls_rs_core::{crypto::SignatureSecretKey, identity::SigningIdentity}; |
| |
| use crate::{ |
| client_config::ClientConfig, |
| group::{ |
| cipher_suite_provider, |
| epoch::SenderDataSecret, |
| key_schedule::{InitSecret, KeySchedule}, |
| proposal::{ExternalInit, Proposal, RemoveProposal}, |
| EpochSecrets, ExternalPubExt, LeafIndex, LeafNode, MlsError, TreeKemPrivate, |
| }, |
| Group, MlsMessage, |
| }; |
| |
| #[cfg(any(feature = "secret_tree_access", feature = "private_message"))] |
| use crate::group::secret_tree::SecretTree; |
| |
| #[cfg(feature = "custom_proposal")] |
| use crate::group::{ |
| framing::MlsMessagePayload, |
| message_processor::{EventOrContent, MessageProcessor}, |
| message_signature::AuthenticatedContent, |
| message_verifier::verify_plaintext_authentication, |
| CustomProposal, |
| }; |
| |
| use alloc::vec; |
| use alloc::vec::Vec; |
| |
| #[cfg(feature = "psk")] |
| use mls_rs_core::psk::{ExternalPskId, PreSharedKey}; |
| |
| #[cfg(feature = "psk")] |
| use crate::group::{ |
| PreSharedKeyProposal, {JustPreSharedKeyID, PreSharedKeyID}, |
| }; |
| |
| use super::{validate_group_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))] |
| pub struct ExternalCommitBuilder<C: ClientConfig> { |
| signer: SignatureSecretKey, |
| signing_identity: SigningIdentity, |
| config: C, |
| tree_data: Option<ExportedTree<'static>>, |
| to_remove: Option<u32>, |
| #[cfg(feature = "psk")] |
| external_psks: Vec<ExternalPskId>, |
| authenticated_data: Vec<u8>, |
| #[cfg(feature = "custom_proposal")] |
| custom_proposals: Vec<Proposal>, |
| #[cfg(feature = "custom_proposal")] |
| received_custom_proposals: Vec<MlsMessage>, |
| } |
| |
| impl<C: ClientConfig> ExternalCommitBuilder<C> { |
| pub(crate) fn new( |
| signer: SignatureSecretKey, |
| signing_identity: SigningIdentity, |
| config: C, |
| ) -> Self { |
| Self { |
| tree_data: None, |
| to_remove: None, |
| authenticated_data: Vec::new(), |
| signer, |
| signing_identity, |
| config, |
| #[cfg(feature = "psk")] |
| external_psks: Vec::new(), |
| #[cfg(feature = "custom_proposal")] |
| custom_proposals: Vec::new(), |
| #[cfg(feature = "custom_proposal")] |
| received_custom_proposals: Vec::new(), |
| } |
| } |
| |
| #[must_use] |
| /// Use external tree data if the GroupInfo message does not contain a |
| /// [`RatchetTreeExt`](crate::extension::built_in::RatchetTreeExt) |
| pub fn with_tree_data(self, tree_data: ExportedTree<'static>) -> Self { |
| Self { |
| tree_data: Some(tree_data), |
| ..self |
| } |
| } |
| |
| #[must_use] |
| /// Propose the removal of an old version of the client as part of the external commit. |
| /// Only one such proposal is allowed. |
| pub fn with_removal(self, to_remove: u32) -> Self { |
| Self { |
| to_remove: Some(to_remove), |
| ..self |
| } |
| } |
| |
| #[must_use] |
| /// Add plaintext authenticated data to the resulting commit message. |
| pub fn with_authenticated_data(self, data: Vec<u8>) -> Self { |
| Self { |
| authenticated_data: data, |
| ..self |
| } |
| } |
| |
| #[cfg(feature = "psk")] |
| #[must_use] |
| /// Add an external psk to the group as part of the external commit. |
| pub fn with_external_psk(mut self, psk: ExternalPskId) -> Self { |
| self.external_psks.push(psk); |
| self |
| } |
| |
| #[cfg(feature = "custom_proposal")] |
| #[must_use] |
| /// Insert a [`CustomProposal`] into the current commit that is being built. |
| pub fn with_custom_proposal(mut self, proposal: CustomProposal) -> Self { |
| self.custom_proposals.push(Proposal::Custom(proposal)); |
| self |
| } |
| |
| #[cfg(all(feature = "custom_proposal", feature = "by_ref_proposal"))] |
| #[must_use] |
| /// Insert a [`CustomProposal`] received from a current group member into the current |
| /// commit that is being built. |
| /// |
| /// # Warning |
| /// |
| /// The authenticity of the proposal is NOT fully verified. It is only verified the |
| /// same way as by [`ExternalGroup`](`crate::external_client::ExternalGroup`). |
| /// The proposal MUST be an MlsPlaintext, else the [`Self::build`] function will fail. |
| pub fn with_received_custom_proposal(mut self, proposal: MlsMessage) -> Self { |
| self.received_custom_proposals.push(proposal); |
| self |
| } |
| |
| /// Build the external commit using a GroupInfo message provided by an existing group member. |
| #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] |
| pub async fn build(self, group_info: MlsMessage) -> Result<(Group<C>, MlsMessage), MlsError> { |
| let protocol_version = group_info.version; |
| |
| if !self.config.version_supported(protocol_version) { |
| return Err(MlsError::UnsupportedProtocolVersion(protocol_version)); |
| } |
| |
| let group_info = group_info |
| .into_group_info() |
| .ok_or(MlsError::UnexpectedMessageType)?; |
| |
| let cipher_suite = cipher_suite_provider( |
| self.config.crypto_provider(), |
| group_info.group_context.cipher_suite, |
| )?; |
| |
| let external_pub_ext = group_info |
| .extensions |
| .get_as::<ExternalPubExt>()? |
| .ok_or(MlsError::MissingExternalPubExtension)?; |
| |
| let public_tree = validate_group_info_joiner( |
| protocol_version, |
| &group_info, |
| self.tree_data, |
| &self.config.identity_provider(), |
| &cipher_suite, |
| ) |
| .await?; |
| |
| let (leaf_node, _) = LeafNode::generate( |
| &cipher_suite, |
| self.config.leaf_properties(), |
| self.signing_identity, |
| &self.signer, |
| self.config.lifetime(), |
| ) |
| .await?; |
| |
| let (init_secret, kem_output) = |
| InitSecret::encode_for_external(&cipher_suite, &external_pub_ext.external_pub).await?; |
| |
| let epoch_secrets = EpochSecrets { |
| #[cfg(feature = "psk")] |
| resumption_secret: PreSharedKey::new(vec![]), |
| sender_data_secret: SenderDataSecret::from(vec![]), |
| #[cfg(any(feature = "secret_tree_access", feature = "private_message"))] |
| secret_tree: SecretTree::empty(), |
| }; |
| |
| let (mut group, _) = Group::join_with( |
| self.config, |
| group_info, |
| public_tree, |
| KeySchedule::new(init_secret), |
| epoch_secrets, |
| TreeKemPrivate::new_for_external(), |
| None, |
| self.signer, |
| ) |
| .await?; |
| |
| #[cfg(feature = "psk")] |
| let psk_ids = self |
| .external_psks |
| .into_iter() |
| .map(|psk_id| PreSharedKeyID::new(JustPreSharedKeyID::External(psk_id), &cipher_suite)) |
| .collect::<Result<Vec<_>, MlsError>>()?; |
| |
| let mut proposals = vec![Proposal::ExternalInit(ExternalInit { kem_output })]; |
| |
| #[cfg(feature = "psk")] |
| proposals.extend( |
| psk_ids |
| .into_iter() |
| .map(|psk| Proposal::Psk(PreSharedKeyProposal { psk })), |
| ); |
| |
| #[cfg(feature = "custom_proposal")] |
| { |
| let mut custom_proposals = self.custom_proposals; |
| proposals.append(&mut custom_proposals); |
| } |
| |
| #[cfg(all(feature = "custom_proposal", feature = "by_ref_proposal"))] |
| for message in self.received_custom_proposals { |
| let MlsMessagePayload::Plain(plaintext) = message.payload else { |
| return Err(MlsError::UnexpectedMessageType); |
| }; |
| |
| let auth_content = AuthenticatedContent::from(plaintext.clone()); |
| |
| verify_plaintext_authentication(&cipher_suite, plaintext, None, None, &group.state) |
| .await?; |
| |
| group |
| .process_event_or_content(EventOrContent::Content(auth_content), true, None) |
| .await?; |
| } |
| |
| if let Some(r) = self.to_remove { |
| proposals.push(Proposal::Remove(RemoveProposal { |
| to_remove: LeafIndex(r), |
| })); |
| } |
| |
| let commit_output = group |
| .commit_internal( |
| proposals, |
| Some(&leaf_node), |
| self.authenticated_data, |
| Default::default(), |
| None, |
| None, |
| ) |
| .await?; |
| |
| group.apply_pending_commit().await?; |
| |
| Ok((group, commit_output.commit_message)) |
| } |
| } |