Skip to main content

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