1 // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 // Copyright by contributors to this project. 3 // SPDX-License-Identifier: (Apache-2.0 OR MIT) 4 5 use mls_rs_core::{crypto::SignatureSecretKey, identity::SigningIdentity}; 6 7 use crate::{ 8 client_config::ClientConfig, 9 group::{ 10 cipher_suite_provider, 11 epoch::SenderDataSecret, 12 key_schedule::{InitSecret, KeySchedule}, 13 proposal::{ExternalInit, Proposal, RemoveProposal}, 14 EpochSecrets, ExternalPubExt, LeafIndex, LeafNode, MlsError, TreeKemPrivate, 15 }, 16 Group, MlsMessage, 17 }; 18 19 #[cfg(any(feature = "secret_tree_access", feature = "private_message"))] 20 use crate::group::secret_tree::SecretTree; 21 22 #[cfg(feature = "custom_proposal")] 23 use crate::group::{ 24 framing::MlsMessagePayload, 25 message_processor::{EventOrContent, MessageProcessor}, 26 message_signature::AuthenticatedContent, 27 message_verifier::verify_plaintext_authentication, 28 CustomProposal, 29 }; 30 31 use alloc::vec; 32 use alloc::vec::Vec; 33 34 #[cfg(feature = "psk")] 35 use mls_rs_core::psk::{ExternalPskId, PreSharedKey}; 36 37 #[cfg(feature = "psk")] 38 use crate::group::{ 39 PreSharedKeyProposal, {JustPreSharedKeyID, PreSharedKeyID}, 40 }; 41 42 use super::{validate_group_info_joiner, ExportedTree}; 43 44 /// A builder that aids with the construction of an external commit. 45 #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::ffi_type(opaque))] 46 pub struct ExternalCommitBuilder<C: ClientConfig> { 47 signer: SignatureSecretKey, 48 signing_identity: SigningIdentity, 49 config: C, 50 tree_data: Option<ExportedTree<'static>>, 51 to_remove: Option<u32>, 52 #[cfg(feature = "psk")] 53 external_psks: Vec<ExternalPskId>, 54 authenticated_data: Vec<u8>, 55 #[cfg(feature = "custom_proposal")] 56 custom_proposals: Vec<Proposal>, 57 #[cfg(feature = "custom_proposal")] 58 received_custom_proposals: Vec<MlsMessage>, 59 } 60 61 impl<C: ClientConfig> ExternalCommitBuilder<C> { new( signer: SignatureSecretKey, signing_identity: SigningIdentity, config: C, ) -> Self62 pub(crate) fn new( 63 signer: SignatureSecretKey, 64 signing_identity: SigningIdentity, 65 config: C, 66 ) -> Self { 67 Self { 68 tree_data: None, 69 to_remove: None, 70 authenticated_data: Vec::new(), 71 signer, 72 signing_identity, 73 config, 74 #[cfg(feature = "psk")] 75 external_psks: Vec::new(), 76 #[cfg(feature = "custom_proposal")] 77 custom_proposals: Vec::new(), 78 #[cfg(feature = "custom_proposal")] 79 received_custom_proposals: Vec::new(), 80 } 81 } 82 83 #[must_use] 84 /// Use external tree data if the GroupInfo message does not contain a 85 /// [`RatchetTreeExt`](crate::extension::built_in::RatchetTreeExt) with_tree_data(self, tree_data: ExportedTree<'static>) -> Self86 pub fn with_tree_data(self, tree_data: ExportedTree<'static>) -> Self { 87 Self { 88 tree_data: Some(tree_data), 89 ..self 90 } 91 } 92 93 #[must_use] 94 /// Propose the removal of an old version of the client as part of the external commit. 95 /// Only one such proposal is allowed. with_removal(self, to_remove: u32) -> Self96 pub fn with_removal(self, to_remove: u32) -> Self { 97 Self { 98 to_remove: Some(to_remove), 99 ..self 100 } 101 } 102 103 #[must_use] 104 /// Add plaintext authenticated data to the resulting commit message. with_authenticated_data(self, data: Vec<u8>) -> Self105 pub fn with_authenticated_data(self, data: Vec<u8>) -> Self { 106 Self { 107 authenticated_data: data, 108 ..self 109 } 110 } 111 112 #[cfg(feature = "psk")] 113 #[must_use] 114 /// Add an external psk to the group as part of the external commit. with_external_psk(mut self, psk: ExternalPskId) -> Self115 pub fn with_external_psk(mut self, psk: ExternalPskId) -> Self { 116 self.external_psks.push(psk); 117 self 118 } 119 120 #[cfg(feature = "custom_proposal")] 121 #[must_use] 122 /// Insert a [`CustomProposal`] into the current commit that is being built. with_custom_proposal(mut self, proposal: CustomProposal) -> Self123 pub fn with_custom_proposal(mut self, proposal: CustomProposal) -> Self { 124 self.custom_proposals.push(Proposal::Custom(proposal)); 125 self 126 } 127 128 #[cfg(all(feature = "custom_proposal", feature = "by_ref_proposal"))] 129 #[must_use] 130 /// Insert a [`CustomProposal`] received from a current group member into the current 131 /// commit that is being built. 132 /// 133 /// # Warning 134 /// 135 /// The authenticity of the proposal is NOT fully verified. It is only verified the 136 /// same way as by [`ExternalGroup`](`crate::external_client::ExternalGroup`). 137 /// The proposal MUST be an MlsPlaintext, else the [`Self::build`] function will fail. with_received_custom_proposal(mut self, proposal: MlsMessage) -> Self138 pub fn with_received_custom_proposal(mut self, proposal: MlsMessage) -> Self { 139 self.received_custom_proposals.push(proposal); 140 self 141 } 142 143 /// Build the external commit using a GroupInfo message provided by an existing group member. 144 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] build(self, group_info: MlsMessage) -> Result<(Group<C>, MlsMessage), MlsError>145 pub async fn build(self, group_info: MlsMessage) -> Result<(Group<C>, MlsMessage), MlsError> { 146 let protocol_version = group_info.version; 147 148 if !self.config.version_supported(protocol_version) { 149 return Err(MlsError::UnsupportedProtocolVersion(protocol_version)); 150 } 151 152 let group_info = group_info 153 .into_group_info() 154 .ok_or(MlsError::UnexpectedMessageType)?; 155 156 let cipher_suite = cipher_suite_provider( 157 self.config.crypto_provider(), 158 group_info.group_context.cipher_suite, 159 )?; 160 161 let external_pub_ext = group_info 162 .extensions 163 .get_as::<ExternalPubExt>()? 164 .ok_or(MlsError::MissingExternalPubExtension)?; 165 166 let public_tree = validate_group_info_joiner( 167 protocol_version, 168 &group_info, 169 self.tree_data, 170 &self.config.identity_provider(), 171 &cipher_suite, 172 ) 173 .await?; 174 175 let (leaf_node, _) = LeafNode::generate( 176 &cipher_suite, 177 self.config.leaf_properties(), 178 self.signing_identity, 179 &self.signer, 180 self.config.lifetime(), 181 ) 182 .await?; 183 184 let (init_secret, kem_output) = 185 InitSecret::encode_for_external(&cipher_suite, &external_pub_ext.external_pub).await?; 186 187 let epoch_secrets = EpochSecrets { 188 #[cfg(feature = "psk")] 189 resumption_secret: PreSharedKey::new(vec![]), 190 sender_data_secret: SenderDataSecret::from(vec![]), 191 #[cfg(any(feature = "secret_tree_access", feature = "private_message"))] 192 secret_tree: SecretTree::empty(), 193 }; 194 195 let (mut group, _) = Group::join_with( 196 self.config, 197 group_info, 198 public_tree, 199 KeySchedule::new(init_secret), 200 epoch_secrets, 201 TreeKemPrivate::new_for_external(), 202 None, 203 self.signer, 204 ) 205 .await?; 206 207 #[cfg(feature = "psk")] 208 let psk_ids = self 209 .external_psks 210 .into_iter() 211 .map(|psk_id| PreSharedKeyID::new(JustPreSharedKeyID::External(psk_id), &cipher_suite)) 212 .collect::<Result<Vec<_>, MlsError>>()?; 213 214 let mut proposals = vec![Proposal::ExternalInit(ExternalInit { kem_output })]; 215 216 #[cfg(feature = "psk")] 217 proposals.extend( 218 psk_ids 219 .into_iter() 220 .map(|psk| Proposal::Psk(PreSharedKeyProposal { psk })), 221 ); 222 223 #[cfg(feature = "custom_proposal")] 224 { 225 let mut custom_proposals = self.custom_proposals; 226 proposals.append(&mut custom_proposals); 227 } 228 229 #[cfg(all(feature = "custom_proposal", feature = "by_ref_proposal"))] 230 for message in self.received_custom_proposals { 231 let MlsMessagePayload::Plain(plaintext) = message.payload else { 232 return Err(MlsError::UnexpectedMessageType); 233 }; 234 235 let auth_content = AuthenticatedContent::from(plaintext.clone()); 236 237 verify_plaintext_authentication(&cipher_suite, plaintext, None, None, &group.state) 238 .await?; 239 240 group 241 .process_event_or_content(EventOrContent::Content(auth_content), true, None) 242 .await?; 243 } 244 245 if let Some(r) = self.to_remove { 246 proposals.push(Proposal::Remove(RemoveProposal { 247 to_remove: LeafIndex(r), 248 })); 249 } 250 251 let commit_output = group 252 .commit_internal( 253 proposals, 254 Some(&leaf_node), 255 self.authenticated_data, 256 Default::default(), 257 None, 258 None, 259 ) 260 .await?; 261 262 group.apply_pending_commit().await?; 263 264 Ok((group, commit_output.commit_message)) 265 } 266 } 267