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    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/// A builder that aids with the construction of an external commit.
47#[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    /// Use external tree data if the GroupInfo message does not contain a
89    /// [`RatchetTreeExt`](crate::extension::built_in::RatchetTreeExt)
90    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    /// Propose the removal of an old version of the client as part of the external commit.
99    /// Only one such proposal is allowed.
100    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    /// Add plaintext authenticated data to the resulting commit message.
109    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    /// Add an external psk to the group as part of the external commit.
119    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    /// Insert a [`CustomProposal`] into the current commit that is being built.
127    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    /// Insert a [`CustomProposal`] received from a current group member into the current
135    /// commit that is being built.
136    ///
137    /// # Warning
138    ///
139    /// The authenticity of the proposal is NOT fully verified. It is only verified the
140    /// same way as by [`ExternalGroup`](`crate::external_client::ExternalGroup`).
141    /// The proposal MUST be an MlsPlaintext, else the [`Self::build`] function will fail.
142    pub fn with_received_custom_proposal(mut self, proposal: MlsMessage) -> Self {
143        self.received_custom_proposals.push(proposal);
144        self
145    }
146
147    /// Change the committer's leaf node extensions as part of making this commit.
148    pub fn with_leaf_node_extensions(self, leaf_node_extensions: ExtensionList) -> Self {
149        Self {
150            leaf_node_extensions,
151            ..self
152        }
153    }
154
155    /// Build the external commit using a GroupInfo message provided by an existing group member.
156    #[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}