Skip to main content

openmls/group/mls_group/commit_builder/
external_commits.rs

1use thiserror::Error;
2use tls_codec::Serialize as _;
3
4#[cfg(doc)]
5use super::CommitMessageBundle;
6
7use crate::{
8    binary_tree::LeafNodeIndex,
9    credentials::CredentialWithKey,
10    error::LibraryError,
11    framing::{ContentType, DecryptedMessage, PublicMessageIn, Sender},
12    group::{
13        commit_builder::{CommitBuilder, ExternalCommitInfo, Initial},
14        past_secrets::MessageSecretsStore,
15        public_group::errors::CreationFromExternalError,
16        ExternalCommitBuilderFinalizeError, LeafNodeLifetimePolicy, MlsGroup, MlsGroupJoinConfig,
17        MlsGroupState, PendingCommitState, ProposalStore, PublicGroup, QueuedProposal,
18        ValidationError, PURE_PLAINTEXT_WIRE_FORMAT_POLICY,
19    },
20    messages::{
21        group_info::VerifiableGroupInfo,
22        proposals::{
23            ExternalInitProposal, PreSharedKeyProposal, Proposal, ProposalOrRefType, ProposalType,
24            RemoveProposal,
25        },
26    },
27    schedule::{psk::store::ResumptionPskStore, EpochSecrets, InitSecret},
28    storage::OpenMlsProvider,
29    treesync::{LeafNodeParameters, RatchetTreeIn},
30    versions::ProtocolVersion,
31};
32
33/// Error type for the [`ExternalCommitBuilder`].
34#[derive(Debug, Error)]
35pub enum ExternalCommitBuilderError<StorageError> {
36    /// See [`LibraryError`] for more details.
37    #[error(transparent)]
38    LibraryError(#[from] LibraryError),
39    /// No ratchet tree available to build initial tree.
40    #[error("No ratchet tree available to build initial tree.")]
41    MissingRatchetTree,
42    /// No external_pub extension available to join group by external commit.
43    #[error("No external_pub extension available to join group by external commit.")]
44    MissingExternalPub,
45    /// We don't support the ciphersuite of the group we are trying to join.
46    #[error("We don't support the ciphersuite of the group we are trying to join.")]
47    UnsupportedCiphersuite,
48    /// This error indicates the public tree is invalid. See
49    /// [`CreationFromExternalError`] for more details.
50    #[error(transparent)]
51    PublicGroupError(#[from] CreationFromExternalError<StorageError>),
52    /// An error occurred when writing group to storage
53    #[error("An error occurred when writing group to storage.")]
54    StorageError(StorageError),
55    /// Error validating proposals.
56    #[error("Error validating proposals: {0}")]
57    InvalidProposal(#[from] ValidationError),
58}
59
60/// This is the builder for external commits. It allows you to build an external
61/// commit that can be used to join a group externally. Parameters such as
62/// optional SelfRemove proposals from other members, the ratchet tree, and the
63/// group join configuration can be set in the first builder stage.
64///
65/// The second stage of this builder is a [`CommitBuilder`] that can be used to
66/// add one or more [`PreSharedKeyProposal`]s to the external commit and specify
67/// [`LeafNodeParameters`].
68#[derive(Default)]
69pub struct ExternalCommitBuilder {
70    proposals: Vec<PublicMessageIn>,
71    ratchet_tree: Option<RatchetTreeIn>,
72    config: MlsGroupJoinConfig,
73    validate_lifetimes: LeafNodeLifetimePolicy,
74    aad: Vec<u8>,
75}
76
77impl MlsGroup {
78    /// Creates a new [`ExternalCommitBuilder`] to build an external commit.
79    pub fn external_commit_builder() -> ExternalCommitBuilder {
80        ExternalCommitBuilder::new()
81    }
82}
83
84impl ExternalCommitBuilder {
85    /// Creates a new [`ExternalCommitBuilder`] with default values.
86    pub fn new() -> Self {
87        Self::default()
88    }
89
90    /// Adds SelfRemove proposals to the external commit. Other proposals or
91    /// other types of messages are ignored.
92    pub fn with_proposals(mut self, proposals: Vec<PublicMessageIn>) -> Self {
93        self.proposals = proposals;
94        self
95    }
96
97    /// Specifies the ratchet tree to use for the external commit. This is only
98    /// used if the ratchet tree is not provided in the [`VerifiableGroupInfo`]
99    /// extensions. A ratchet tree must be provided, either in the
100    /// [`VerifiableGroupInfo`] extensions or via this method.
101    pub fn with_ratchet_tree(mut self, ratchet_tree: RatchetTreeIn) -> Self {
102        self.ratchet_tree = Some(ratchet_tree);
103        self
104    }
105
106    /// Specifies the configuration to use for the group built as part of the
107    /// external commit. Note that the external commit will always be a
108    /// `PublicMessage` regardless of the wire format policy set in the group
109    /// config.
110    pub fn with_config(mut self, config: MlsGroupJoinConfig) -> Self {
111        self.config = config;
112        self
113    }
114
115    /// Specifies additional authenticated data (AAD) to be included in the
116    /// external commit.
117    pub fn with_aad(mut self, aad: Vec<u8>) -> Self {
118        self.aad = aad;
119        self
120    }
121
122    /// Skip the validation of lifetimes in leaf nodes in the ratchet tree.
123    /// Note that only the leaf nodes are checked that were never updated.
124    ///
125    /// By default they are validated.
126    pub fn skip_lifetime_validation(mut self) -> Self {
127        self.validate_lifetimes = LeafNodeLifetimePolicy::Skip;
128        self
129    }
130
131    /// Build the [`MlsGroup`] from the provided [`VerifiableGroupInfo`] and
132    /// [`CredentialWithKey`].
133    ///
134    /// Returns a [`CommitBuilder`] that can be used to further configure the
135    /// external commit.
136    pub fn build_group<Provider: OpenMlsProvider>(
137        self,
138        provider: &Provider,
139        verifiable_group_info: VerifiableGroupInfo,
140        credential_with_key: CredentialWithKey,
141    ) -> Result<
142        CommitBuilder<'_, Initial, MlsGroup>,
143        ExternalCommitBuilderError<Provider::StorageError>,
144    > {
145        let ExternalCommitBuilder {
146            proposals,
147            ratchet_tree,
148            mut config,
149            aad,
150            validate_lifetimes,
151        } = self;
152
153        // Build the ratchet tree
154
155        // Set nodes either from the extension or from the `ratchet_tree`.
156        let ratchet_tree = match verifiable_group_info.extensions().ratchet_tree() {
157            Some(extension) => extension.ratchet_tree().clone(),
158            None => match ratchet_tree {
159                Some(ratchet_tree) => ratchet_tree,
160                None => return Err(ExternalCommitBuilderError::MissingRatchetTree),
161            },
162        };
163
164        let (public_group, group_info) = PublicGroup::from_ratchet_tree(
165            provider.crypto(),
166            ratchet_tree,
167            verifiable_group_info,
168            ProposalStore::new(),
169            validate_lifetimes,
170        )?;
171        let group_context = public_group.group_context();
172
173        // Obtain external_pub from GroupInfo extensions.
174        let external_pub = group_info
175            .extensions()
176            .external_pub()
177            .ok_or(ExternalCommitBuilderError::MissingExternalPub)?
178            .external_pub();
179
180        let (init_secret, kem_output) = InitSecret::from_group_context(
181            provider.crypto(),
182            group_context,
183            external_pub.as_slice(),
184        )
185        .map_err(|_| ExternalCommitBuilderError::UnsupportedCiphersuite)?;
186
187        // The `EpochSecrets` we create here are essentially zero, with the
188        // exception of the `InitSecret`, which is all we need here for the
189        // external commit.
190        let ciphersuite = group_context.ciphersuite();
191        let epoch_secrets =
192            EpochSecrets::with_init_secret(provider.crypto(), ciphersuite, init_secret)
193                .map_err(LibraryError::unexpected_crypto_error)?;
194        let (group_epoch_secrets, message_secrets) = epoch_secrets.split_secrets(
195            group_context
196                .tls_serialize_detached()
197                .map_err(LibraryError::missing_bound_check)?,
198            public_group.tree_size(),
199            // We use a fake own index of 0 here, as we're not going to use the
200            // tree for encryption until after the first commit. This issue is
201            // tracked in #767.
202            LeafNodeIndex::new(0u32),
203        );
204        let message_secrets_store = MessageSecretsStore::new_with_secret(
205            config.past_epoch_deletion_policy(),
206            message_secrets,
207        );
208
209        let external_init_proposal =
210            Proposal::external_init(ExternalInitProposal::from(kem_output));
211
212        // Authenticate the proposals as best as we can
213        let serialized_context = group_context
214            .tls_serialize_detached()
215            .map_err(LibraryError::missing_bound_check)?;
216        let mut queued_proposals = Vec::new();
217        for message in proposals {
218            if message.content_type() != ContentType::Proposal {
219                continue; // We only want proposals.
220            }
221            let decrypted_message = DecryptedMessage::from_inbound_public_message(
222                message,
223                None,
224                serialized_context.clone(),
225                provider.crypto(),
226                ciphersuite,
227            )?;
228            let unverified_message = public_group.parse_message(decrypted_message, None)?;
229            let (verified_message, _credential) = unverified_message.verify(
230                ciphersuite,
231                provider.crypto(),
232                ProtocolVersion::default(),
233            )?;
234            let queued_proposal = QueuedProposal::from_authenticated_content(
235                ciphersuite,
236                provider.crypto(),
237                verified_message,
238                ProposalOrRefType::Reference,
239            )?;
240            // We ignore any proposal that is not a SelfRemove.
241            if queued_proposal.proposal().is_type(ProposalType::SelfRemove) {
242                queued_proposals.push(queued_proposal);
243            }
244        }
245
246        let inline_proposals = [external_init_proposal].into_iter();
247
248        // If there is a group member in the group with the same identity as us,
249        // commit a remove proposal.
250        let our_signature_key = credential_with_key.signature_key.as_slice();
251        let remove_proposal = public_group.members().find_map(|member| {
252            (member.signature_key == our_signature_key).then_some(Proposal::remove(
253                RemoveProposal {
254                    removed: member.index,
255                },
256            ))
257        });
258
259        let inline_proposals = inline_proposals
260            .chain(remove_proposal)
261            .map(|p| {
262                QueuedProposal::from_proposal_and_sender(
263                    ciphersuite,
264                    provider.crypto(),
265                    p,
266                    &Sender::NewMemberCommit,
267                )
268            })
269            .collect::<Result<Vec<_>, _>>()?;
270
271        queued_proposals.extend(inline_proposals);
272
273        let own_leaf_index = public_group.leftmost_free_index(queued_proposals.iter())?;
274
275        let original_wire_format_policy = config.wire_format_policy;
276
277        // We set this to PURE_PLAINTEXT_WIRE_FORMAT_POLICY so that the
278        // external commit can be sent as a PublicMessageIn. The wire format
279        // policy will be set to the original wire format policy after the
280        // external commit has been sent.
281        config.wire_format_policy = PURE_PLAINTEXT_WIRE_FORMAT_POLICY;
282
283        let mut mls_group = MlsGroup {
284            mls_group_config: config,
285            own_leaf_nodes: vec![],
286            aad: vec![],
287            group_state: MlsGroupState::Operational,
288            public_group,
289            group_epoch_secrets,
290            own_leaf_index,
291            message_secrets_store,
292            resumption_psk_store: ResumptionPskStore::new(32),
293            // This is set to `None` for now. It will be set once the external
294            // commit is merged.
295            #[cfg(feature = "extensions-draft-08")]
296            application_export_tree: None,
297        };
298
299        // Add all proposals to the proposal store.
300        let proposal_store = mls_group.proposal_store_mut();
301        for queued_proposal in queued_proposals {
302            proposal_store.add(queued_proposal);
303        }
304
305        let mut commit_builder = CommitBuilder::<'_, Initial, MlsGroup>::new(mls_group);
306
307        commit_builder.stage.force_self_update = true;
308        commit_builder.stage.external_commit_info = Some(ExternalCommitInfo {
309            wire_format_policy: original_wire_format_policy,
310            credential: credential_with_key.clone(),
311            aad,
312        });
313        let leaf_node_parameters = LeafNodeParameters::builder()
314            .with_credential_with_key(credential_with_key)
315            .build();
316        commit_builder.stage.leaf_node_parameters = leaf_node_parameters;
317
318        Ok(commit_builder)
319    }
320}
321
322// Impls that only apply to external commits.
323impl<'a> CommitBuilder<'a, Initial, MlsGroup> {
324    /// Adds a [`PreSharedKeyProposal`] to the proposals to be committed.
325    pub fn add_psk_proposal(mut self, proposal: PreSharedKeyProposal) -> Self {
326        self.stage.own_proposals.push(Proposal::psk(proposal));
327        self
328    }
329
330    /// Adds the [`PreSharedKeyProposal`] in the iterator to the proposals to be
331    /// committed.
332    pub fn add_psk_proposals(
333        mut self,
334        proposals: impl IntoIterator<Item = PreSharedKeyProposal>,
335    ) -> Self {
336        self.stage
337            .own_proposals
338            .extend(proposals.into_iter().map(Proposal::psk));
339        self
340    }
341}
342
343// Impls that apply only to external commits.
344impl CommitBuilder<'_, super::Complete, MlsGroup> {
345    /// Finalizes and returns the [`MlsGroup`], as well as the
346    /// [`CommitMessageBundle`].
347    ///
348    /// In contrast to the deprecated [`MlsGroup::join_by_external_commit`]
349    /// there is no need to merge the pending commit.
350    pub fn finalize<Provider: OpenMlsProvider>(
351        self,
352        provider: &Provider,
353    ) -> Result<
354        (MlsGroup, super::CommitMessageBundle),
355        ExternalCommitBuilderFinalizeError<Provider::StorageError>,
356    > {
357        let Self {
358            mut group,
359            stage:
360                super::Complete {
361                    result: create_commit_result,
362                    original_wire_format_policy,
363                },
364            ..
365        } = self;
366
367        // Convert AuthenticatedContent messages to MLSMessage.
368        let mls_message = group.content_to_mls_message(create_commit_result.commit, provider)?;
369
370        group.reset_aad();
371
372        // Restore the original wire format policy.
373        if let Some(wire_format_policy) = original_wire_format_policy {
374            group.mls_group_config.wire_format_policy = wire_format_policy;
375        }
376
377        // Store the group in storage.
378        group
379            .store(provider.storage())
380            .map_err(ExternalCommitBuilderFinalizeError::StorageError)?;
381
382        // Set the current group state to [`MlsGroupState::PendingCommit`],
383        // storing the current [`StagedCommit`] from the commit results
384        group.group_state = MlsGroupState::PendingCommit(Box::new(PendingCommitState::Member(
385            create_commit_result.staged_commit,
386        )));
387
388        group.merge_pending_commit(provider)?;
389
390        let bundle = super::CommitMessageBundle {
391            version: group.version(),
392            commit: mls_message,
393            welcome: create_commit_result.welcome_option,
394            group_info: create_commit_result.group_info,
395        };
396
397        Ok((group, bundle))
398    }
399}