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