blob: 34b10427e32b06b0f5083aa6d27767aa709148a9 [file] [log] [blame]
Martin Geisler51f31cc2024-04-09 13:35:45 +02001// 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
5use mls_rs_core::{crypto::SignatureSecretKey, identity::SigningIdentity};
6
7use 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"))]
20use crate::group::secret_tree::SecretTree;
21
22#[cfg(feature = "custom_proposal")]
23use crate::group::{
24 framing::MlsMessagePayload,
25 message_processor::{EventOrContent, MessageProcessor},
26 message_signature::AuthenticatedContent,
27 message_verifier::verify_plaintext_authentication,
28 CustomProposal,
29};
30
31use alloc::vec;
32use alloc::vec::Vec;
33
34#[cfg(feature = "psk")]
35use mls_rs_core::psk::{ExternalPskId, PreSharedKey};
36
37#[cfg(feature = "psk")]
38use crate::group::{
39 PreSharedKeyProposal, {JustPreSharedKeyID, PreSharedKeyID},
40};
41
42use 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))]
46pub 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
61impl<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}