Martin Geisler | 51f31cc | 2024-04-09 13:35:45 +0200 | [diff] [blame^] | 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> { |
| 62 | 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) |
| 86 | 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. |
| 96 | 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. |
| 105 | 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. |
| 115 | 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. |
| 123 | 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. |
| 138 | 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)] |
| 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 | } |