mls_rs/group/
external_commit.rs

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
5use 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/// A builder that aids with the construction of an external commit.
48#[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    /// Use external tree data if the GroupInfo message does not contain a
92    /// [`RatchetTreeExt`](crate::extension::built_in::RatchetTreeExt)
93    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    /// Propose the removal of an old version of the client as part of the external commit.
102    /// Only one such proposal is allowed.
103    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    /// Add plaintext authenticated data to the resulting commit message.
112    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    /// Add an external psk to the group as part of the external commit.
122    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    /// Insert a [`CustomProposal`] into the current commit that is being built.
130    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    /// Insert a [`CustomProposal`] received from a current group member into the current
138    /// commit that is being built.
139    ///
140    /// # Warning
141    ///
142    /// The authenticity of the proposal is NOT fully verified. It is only verified the
143    /// same way as by [`ExternalGroup`](`crate::external_client::ExternalGroup`).
144    /// The proposal MUST be an MlsPlaintext, else the [`Self::build`] function will fail.
145    pub fn with_received_custom_proposal(mut self, proposal: MlsMessage) -> Self {
146        self.received_custom_proposals.push(proposal);
147        self
148    }
149
150    /// Change the committer's leaf node extensions as part of making this commit.
151    pub fn with_leaf_node_extensions(self, leaf_node_extensions: ExtensionList) -> Self {
152        Self {
153            leaf_node_extensions,
154            ..self
155        }
156    }
157
158    /// Add a time to associate with the commit creation.
159    pub fn commit_time(self, commit_time: MlsTime) -> Self {
160        Self {
161            commit_time: Some(commit_time),
162            ..self
163        }
164    }
165
166    /// Build the external commit using a GroupInfo message provided by an existing group member.
167    #[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}