1use mls_rs_core::{
6 crypto::SignatureSecretKey, extension::ExtensionList, identity::SigningIdentity,
7};
8
9use crate::{
10 client_config::ClientConfig,
11 group::{
12 cipher_suite_provider,
13 epoch::SenderDataSecret,
14 key_schedule::{InitSecret, KeySchedule},
15 proposal::{ExternalInit, Proposal, RemoveProposal},
16 EpochSecrets, ExternalPubExt, LeafIndex, LeafNode, MlsError, TreeKemPrivate,
17 },
18 time::MlsTime,
19 Group, MlsMessage,
20};
21
22#[cfg(any(feature = "secret_tree_access", feature = "private_message"))]
23use crate::group::secret_tree::SecretTree;
24
25#[cfg(feature = "custom_proposal")]
26use crate::group::{
27 framing::MlsMessagePayload,
28 message_processor::{EventOrContent, MessageProcessor},
29 message_signature::AuthenticatedContent,
30 message_verifier::verify_plaintext_authentication,
31 CustomProposal, SignaturePublicKeysContainer,
32};
33
34use alloc::vec;
35use alloc::vec::Vec;
36
37#[cfg(feature = "psk")]
38use mls_rs_core::psk::{ExternalPskId, PreSharedKey};
39
40#[cfg(feature = "psk")]
41use crate::group::{
42 PreSharedKeyProposal, {JustPreSharedKeyID, PreSharedKeyID},
43};
44
45use super::{validate_tree_and_info_joiner, ExportedTree};
46
47#[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::ffi_type(opaque))]
49pub struct ExternalCommitBuilder<C: ClientConfig> {
50 signer: SignatureSecretKey,
51 signing_identity: SigningIdentity,
52 leaf_node_extensions: ExtensionList,
53 config: C,
54 tree_data: Option<ExportedTree<'static>>,
55 to_remove: Option<u32>,
56 #[cfg(feature = "psk")]
57 external_psks: Vec<ExternalPskId>,
58 authenticated_data: Vec<u8>,
59 #[cfg(feature = "custom_proposal")]
60 custom_proposals: Vec<Proposal>,
61 #[cfg(feature = "custom_proposal")]
62 received_custom_proposals: Vec<MlsMessage>,
63 commit_time: Option<MlsTime>,
64}
65
66impl<C: ClientConfig> ExternalCommitBuilder<C> {
67 pub(crate) fn new(
68 signer: SignatureSecretKey,
69 signing_identity: SigningIdentity,
70 config: C,
71 ) -> Self {
72 Self {
73 tree_data: None,
74 to_remove: None,
75 authenticated_data: Vec::new(),
76 signer,
77 signing_identity,
78 leaf_node_extensions: Default::default(),
79 config,
80 #[cfg(feature = "psk")]
81 external_psks: Vec::new(),
82 #[cfg(feature = "custom_proposal")]
83 custom_proposals: Vec::new(),
84 #[cfg(feature = "custom_proposal")]
85 received_custom_proposals: Vec::new(),
86 commit_time: None,
87 }
88 }
89
90 #[must_use]
91 pub fn with_tree_data(self, tree_data: ExportedTree<'static>) -> Self {
94 Self {
95 tree_data: Some(tree_data),
96 ..self
97 }
98 }
99
100 #[must_use]
101 pub fn with_removal(self, to_remove: u32) -> Self {
104 Self {
105 to_remove: Some(to_remove),
106 ..self
107 }
108 }
109
110 #[must_use]
111 pub fn with_authenticated_data(self, data: Vec<u8>) -> Self {
113 Self {
114 authenticated_data: data,
115 ..self
116 }
117 }
118
119 #[cfg(feature = "psk")]
120 #[must_use]
121 pub fn with_external_psk(mut self, psk: ExternalPskId) -> Self {
123 self.external_psks.push(psk);
124 self
125 }
126
127 #[cfg(feature = "custom_proposal")]
128 #[must_use]
129 pub fn with_custom_proposal(mut self, proposal: CustomProposal) -> Self {
131 self.custom_proposals.push(Proposal::Custom(proposal));
132 self
133 }
134
135 #[cfg(all(feature = "custom_proposal", feature = "by_ref_proposal"))]
136 #[must_use]
137 pub fn with_received_custom_proposal(mut self, proposal: MlsMessage) -> Self {
146 self.received_custom_proposals.push(proposal);
147 self
148 }
149
150 pub fn with_leaf_node_extensions(self, leaf_node_extensions: ExtensionList) -> Self {
152 Self {
153 leaf_node_extensions,
154 ..self
155 }
156 }
157
158 pub fn commit_time(self, commit_time: MlsTime) -> Self {
160 Self {
161 commit_time: Some(commit_time),
162 ..self
163 }
164 }
165
166 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
168 pub async fn build(self, group_info: MlsMessage) -> Result<(Group<C>, MlsMessage), MlsError> {
169 let protocol_version = group_info.version;
170
171 if !self.config.version_supported(protocol_version) {
172 return Err(MlsError::UnsupportedProtocolVersion(protocol_version));
173 }
174
175 let group_info = group_info
176 .into_group_info()
177 .ok_or(MlsError::UnexpectedMessageType)?;
178
179 let cipher_suite = cipher_suite_provider(
180 self.config.crypto_provider(),
181 group_info.group_context.cipher_suite,
182 )?;
183
184 let external_pub_ext = group_info
185 .extensions
186 .get_as::<ExternalPubExt>()?
187 .ok_or(MlsError::MissingExternalPubExtension)?;
188
189 let public_tree = validate_tree_and_info_joiner(
190 protocol_version,
191 &group_info,
192 self.tree_data,
193 &self.config.identity_provider(),
194 &cipher_suite,
195 self.commit_time,
196 )
197 .await?;
198
199 let (leaf_node, _) = LeafNode::generate(
200 &cipher_suite,
201 self.config.leaf_properties(self.leaf_node_extensions),
202 self.signing_identity,
203 &self.signer,
204 self.config.lifetime(self.commit_time),
205 )
206 .await?;
207
208 let (init_secret, kem_output) =
209 InitSecret::encode_for_external(&cipher_suite, &external_pub_ext.external_pub).await?;
210
211 let epoch_secrets = EpochSecrets {
212 #[cfg(feature = "psk")]
213 resumption_secret: PreSharedKey::new(vec![]),
214 sender_data_secret: SenderDataSecret::from(vec![]),
215 #[cfg(any(feature = "secret_tree_access", feature = "private_message"))]
216 secret_tree: SecretTree::empty(),
217 };
218
219 let (mut group, _) = Group::join_with(
220 self.config,
221 group_info,
222 public_tree,
223 KeySchedule::new(init_secret),
224 epoch_secrets,
225 TreeKemPrivate::new_for_external(),
226 None,
227 self.signer,
228 )
229 .await?;
230
231 #[cfg(feature = "psk")]
232 let psk_ids = self
233 .external_psks
234 .into_iter()
235 .map(|psk_id| PreSharedKeyID::new(JustPreSharedKeyID::External(psk_id), &cipher_suite))
236 .collect::<Result<Vec<_>, MlsError>>()?;
237
238 let mut proposals = vec![Proposal::ExternalInit(ExternalInit { kem_output })];
239
240 #[cfg(feature = "psk")]
241 proposals.extend(
242 psk_ids
243 .into_iter()
244 .map(|psk| Proposal::Psk(PreSharedKeyProposal { psk })),
245 );
246
247 #[cfg(feature = "custom_proposal")]
248 {
249 let mut custom_proposals = self.custom_proposals;
250 proposals.append(&mut custom_proposals);
251 }
252
253 #[cfg(all(feature = "custom_proposal", feature = "by_ref_proposal"))]
254 for message in self.received_custom_proposals {
255 let MlsMessagePayload::Plain(plaintext) = message.payload else {
256 return Err(MlsError::UnexpectedMessageType);
257 };
258
259 let auth_content = AuthenticatedContent::from(plaintext.clone());
260 verify_plaintext_authentication(
261 &cipher_suite,
262 plaintext,
263 None,
264 &group.state.context,
265 SignaturePublicKeysContainer::RatchetTree(&group.state.public_tree),
266 )
267 .await?;
268
269 group
270 .process_event_or_content(EventOrContent::Content(auth_content), true, None)
271 .await?;
272 }
273
274 if let Some(r) = self.to_remove {
275 proposals.push(Proposal::Remove(RemoveProposal {
276 to_remove: LeafIndex::try_from(r)?,
277 }));
278 }
279
280 let (commit_output, pending_commit) = group
281 .commit_internal(
282 proposals,
283 Some(&leaf_node),
284 self.authenticated_data,
285 Default::default(),
286 None,
287 None,
288 None,
289 self.commit_time,
290 )
291 .await?;
292
293 group.pending_commit = pending_commit.try_into()?;
294 group.apply_pending_commit().await?;
295
296 Ok((group, commit_output.commit_message))
297 }
298}