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