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