Import 'mls-rs' crate Request Document: go/android-rust-importing-crates For CL Reviewers: go/android3p#cl-review For Build Team: go/ab-third-party-imports Bug: http://b/330708876 Test: m libmls_rs Change-Id: Ib0a891a4d7bf582ebea9ba7a1447ea959e42e0d3
diff --git a/src/group/external_commit.rs b/src/group/external_commit.rs new file mode 100644 index 0000000..34b1042 --- /dev/null +++ b/src/group/external_commit.rs
@@ -0,0 +1,266 @@ +// 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)) + } +}