openmls/group/public_group/
mod.rs

1//! # Public Groups
2//!
3//! There are a few use-cases that require the tracking of an MLS group based on
4//! [`PublicMessage`]s, e.g. for group membership tracking by a delivery
5//! service.
6//!
7//! This module and its submodules contain the [`PublicGroup`] struct, as well
8//! as associated helper structs the goal of which is to enable this
9//! functionality.
10//!
11//! To avoid duplication of code and functionality, [`MlsGroup`] internally
12//! relies on a [`PublicGroup`] as well.
13
14use std::collections::HashSet;
15
16use openmls_traits::{crypto::OpenMlsCrypto, types::Ciphersuite};
17use serde::{Deserialize, Serialize};
18
19use self::{
20    diff::{PublicGroupDiff, StagedPublicGroupDiff},
21    errors::CreationFromExternalError,
22};
23use super::{
24    proposal_store::{ProposalStore, QueuedProposal},
25    GroupContext, GroupId, Member, StagedCommit,
26};
27#[cfg(test)]
28use crate::treesync::{node::parent_node::PlainUpdatePathNode, treekem::UpdatePathNode};
29use crate::{
30    binary_tree::{
31        array_representation::{direct_path, TreeSize},
32        LeafNodeIndex,
33    },
34    ciphersuite::{hash_ref::ProposalRef, signable::Verifiable},
35    error::LibraryError,
36    extensions::RequiredCapabilitiesExtension,
37    framing::InterimTranscriptHashInput,
38    messages::{
39        group_info::{GroupInfo, VerifiableGroupInfo},
40        proposals::{Proposal, ProposalOrRefType, ProposalType},
41        ConfirmationTag, PathSecret,
42    },
43    schedule::CommitSecret,
44    storage::PublicStorageProvider,
45    treesync::{
46        errors::{DerivePathError, TreeSyncFromNodesError},
47        node::{
48            encryption_keys::{EncryptionKey, EncryptionKeyPair},
49            leaf_node::LeafNode,
50        },
51        RatchetTree, RatchetTreeIn, TreeSync,
52    },
53    versions::ProtocolVersion,
54};
55#[cfg(doc)]
56use crate::{framing::PublicMessage, group::MlsGroup};
57
58pub(crate) mod builder;
59pub(crate) mod diff;
60pub mod errors;
61pub mod process;
62pub(crate) mod staged_commit;
63#[cfg(test)]
64mod tests;
65mod validation;
66
67/// This struct holds all public values of an MLS group.
68#[derive(Debug)]
69#[cfg_attr(any(test, feature = "test-utils"), derive(PartialEq, Clone))]
70pub struct PublicGroup {
71    treesync: TreeSync,
72    proposal_store: ProposalStore,
73    group_context: GroupContext,
74    interim_transcript_hash: Vec<u8>,
75    // Most recent confirmation tag. Kept here for verification purposes.
76    confirmation_tag: ConfirmationTag,
77}
78
79/// This is a wrapper type, because we can't implement the storage traits on `Vec<u8>`.
80#[derive(Debug, Serialize, Deserialize)]
81pub struct InterimTranscriptHash(pub Vec<u8>);
82
83impl PublicGroup {
84    /// Create a new PublicGroup from a [`TreeSync`] instance and a
85    /// [`GroupInfo`].
86    pub(crate) fn new(
87        crypto: &impl OpenMlsCrypto,
88        treesync: TreeSync,
89        group_context: GroupContext,
90        initial_confirmation_tag: ConfirmationTag,
91    ) -> Result<Self, LibraryError> {
92        let interim_transcript_hash = {
93            let input = InterimTranscriptHashInput::from(&initial_confirmation_tag);
94
95            input.calculate_interim_transcript_hash(
96                crypto,
97                group_context.ciphersuite(),
98                group_context.confirmed_transcript_hash(),
99            )?
100        };
101
102        Ok(PublicGroup {
103            treesync,
104            proposal_store: ProposalStore::new(),
105            group_context,
106            interim_transcript_hash,
107            confirmation_tag: initial_confirmation_tag,
108        })
109    }
110
111    /// Create a [`PublicGroup`] instance to start tracking an existing MLS group.
112    ///
113    /// This function performs basic validation checks and returns an error if
114    /// one of the checks fails. See [`CreationFromExternalError`] for more
115    /// details.
116    pub fn from_external<StorageProvider, StorageError>(
117        crypto: &impl OpenMlsCrypto,
118        storage: &StorageProvider,
119        ratchet_tree: RatchetTreeIn,
120        verifiable_group_info: VerifiableGroupInfo,
121        proposal_store: ProposalStore,
122    ) -> Result<(Self, GroupInfo), CreationFromExternalError<StorageError>>
123    where
124        StorageProvider: PublicStorageProvider<Error = StorageError>,
125    {
126        let (public_group, group_info) = PublicGroup::from_external_internal(
127            crypto,
128            ratchet_tree,
129            verifiable_group_info,
130            proposal_store,
131        )?;
132
133        public_group
134            .store(storage)
135            .map_err(CreationFromExternalError::WriteToStorageError)?;
136
137        Ok((public_group, group_info))
138    }
139    pub(crate) fn from_external_internal<StorageError>(
140        crypto: &impl OpenMlsCrypto,
141        ratchet_tree: RatchetTreeIn,
142        verifiable_group_info: VerifiableGroupInfo,
143        proposal_store: ProposalStore,
144    ) -> Result<(Self, GroupInfo), CreationFromExternalError<StorageError>> {
145        let ciphersuite = verifiable_group_info.ciphersuite();
146
147        let group_id = verifiable_group_info.group_id();
148        let ratchet_tree = ratchet_tree
149            .into_verified(ciphersuite, crypto, group_id)
150            .map_err(|e| {
151                CreationFromExternalError::TreeSyncError(TreeSyncFromNodesError::RatchetTreeError(
152                    e,
153                ))
154            })?;
155
156        // Create a RatchetTree from the given nodes. We have to do this before
157        // verifying the group info, since we need to find the Credential to verify the
158        // signature against.
159        let treesync = TreeSync::from_ratchet_tree(crypto, ciphersuite, ratchet_tree)?;
160
161        let mut encryption_keys = HashSet::new();
162
163        // Perform basic checks that the leaf nodes in the ratchet tree are valid
164        // These checks only do those that don't need group context. We do the full
165        // checks later, but do these here to fail early in case of funny business
166        treesync.full_leaves().try_for_each(|leaf_node| {
167            leaf_node.validate_locally()?;
168
169            // Check that no two nodes share an encryption key.
170            // This is a bit stronger than what the spec requires: It requires that the encryption keys
171            // in parent nodes and unmerged leaves must be unique. Here, we check that all encryption
172            // keys (all leaf nodes, incl. unmerged and all parent nodes) are unique.
173            //
174            // https://validation.openmls.tech/#valn1410
175            if !encryption_keys.insert(leaf_node.encryption_key()) {
176                return Err(CreationFromExternalError::DuplicateEncryptionKey);
177            }
178
179            Ok(())
180        })?;
181
182        // For each non-empty parent node and each entry in the node's unmerged_leaves field:
183        treesync
184            .full_parents()
185            .try_for_each(|(parent_index, parent_node)| {
186                // Check that no two nodes share an encryption key.
187                // This is a bit stronger than what the spec requires: It requires that the encryption keys
188                // in parent nodes and unmerged leaves must be unique. Here, we check that all encryption
189                // keys (all leaf nodes, incl. unmerged and all parent nodes) are unique.
190                //
191                // https://validation.openmls.tech/#valn1410
192                if !encryption_keys.insert(parent_node.encryption_key()) {
193                    return Err(CreationFromExternalError::DuplicateEncryptionKey);
194                }
195
196                parent_node
197                    .unmerged_leaves()
198                    .iter()
199                    .try_for_each(|leaf_index| {
200                        let path = direct_path(*leaf_index, treesync.tree_size());
201
202                        // https://validation.openmls.tech/#valn1408
203                        // Verify that the entry represents a non-blank leaf node that is a descendant of the
204                        // parent node.
205                        let this_parent_offset = path
206                            .iter()
207                            .position(|x| x == &parent_index)
208                            .ok_or(
209                            CreationFromExternalError::<StorageError>::UnmergedLeafNotADescendant,
210                        )?;
211                        let path_leaf_to_this = &path[..this_parent_offset];
212
213
214                        // https://validation.openmls.tech/#valn1409
215                        // Verify that every non-blank intermediate node between the leaf node and the parent
216                        // node also has an entry for the leaf node in its unmerged_leaves.
217                        path_leaf_to_this
218                            .iter()
219                            .try_for_each(|intermediate_index| {
220                                // None would be blank, and we don't care about those
221                                if let Some(intermediate_node) = treesync
222                                    .parent(*intermediate_index) {
223                                    if !intermediate_node.unmerged_leaves().contains(leaf_index) {
224                                        return Err(CreationFromExternalError::<StorageError>::IntermediateNodeMissingUnmergedLeaf);
225                                    }
226                                }
227
228                                Ok(())
229                            })
230                    })
231            })?;
232
233        // https://validation.openmls.tech/#valn1402
234        let group_info: GroupInfo = {
235            let signer_signature_key = treesync
236                .leaf(verifiable_group_info.signer())
237                .ok_or(CreationFromExternalError::UnknownSender)?
238                .signature_key()
239                .clone()
240                .into_signature_public_key_enriched(ciphersuite.signature_algorithm());
241
242            verifiable_group_info
243                .verify(crypto, &signer_signature_key)
244                .map_err(|_| CreationFromExternalError::InvalidGroupInfoSignature)?
245        };
246
247        // https://validation.openmls.tech/#valn1405
248        if treesync.tree_hash() != group_info.group_context().tree_hash() {
249            return Err(CreationFromExternalError::TreeHashMismatch);
250        }
251
252        if group_info.group_context().protocol_version() != ProtocolVersion::Mls10 {
253            return Err(CreationFromExternalError::UnsupportedMlsVersion);
254        }
255
256        let group_context = group_info.group_context().clone();
257
258        let interim_transcript_hash = {
259            let input = InterimTranscriptHashInput::from(group_info.confirmation_tag());
260
261            input.calculate_interim_transcript_hash(
262                crypto,
263                group_context.ciphersuite(),
264                group_context.confirmed_transcript_hash(),
265            )?
266        };
267
268        let public_group = Self {
269            treesync,
270            group_context,
271            interim_transcript_hash,
272            confirmation_tag: group_info.confirmation_tag().clone(),
273            proposal_store,
274        };
275
276        // Fully check that the leaf nodes in the ratchet tree are valid
277        // https://validation.openmls.tech/#valn1407
278        public_group
279            .treesync
280            .full_leaves()
281            .try_for_each(|leaf_node| public_group.validate_leaf_node(leaf_node))?;
282
283        Ok((public_group, group_info))
284    }
285
286    /// Returns the index of the sender of a staged, external commit.
287    pub fn ext_commit_sender_index(
288        &self,
289        commit: &StagedCommit,
290    ) -> Result<LeafNodeIndex, LibraryError> {
291        self.leftmost_free_index(commit.queued_proposals().filter_map(|p| {
292            if matches!(p.proposal_or_ref_type(), ProposalOrRefType::Proposal) {
293                Some(Some(p.proposal()))
294            } else {
295                None
296            }
297        }))
298    }
299
300    /// Returns the leftmost free leaf index.
301    ///
302    /// For External Commits of the "resync" type, this returns the index
303    /// of the sender.
304    ///
305    /// The proposals must be validated before calling this function.
306    pub(crate) fn leftmost_free_index<'a>(
307        &self,
308        mut inline_proposals: impl Iterator<Item = Option<&'a Proposal>>,
309    ) -> Result<LeafNodeIndex, LibraryError> {
310        // Leftmost free leaf in the tree
311        let free_leaf_index = self.treesync().free_leaf_index();
312        // Returns the first remove proposal (if there is one)
313        let remove_proposal_option = inline_proposals
314            .find(|proposal| match proposal {
315                Some(p) => p.is_type(ProposalType::Remove),
316                None => false,
317            })
318            .flatten();
319        let leaf_index = if let Some(remove_proposal) = remove_proposal_option {
320            if let Proposal::Remove(remove_proposal) = remove_proposal {
321                let removed_index = remove_proposal.removed();
322                // The committer should always be in the left-most leaf.
323                if removed_index < free_leaf_index {
324                    removed_index
325                } else {
326                    free_leaf_index
327                }
328            } else {
329                return Err(LibraryError::custom("missing key package"));
330            }
331        } else {
332            free_leaf_index
333        };
334        Ok(leaf_index)
335    }
336
337    /// Create an empty  [`PublicGroupDiff`] based on this [`PublicGroup`].
338    pub(crate) fn empty_diff(&self) -> PublicGroupDiff {
339        PublicGroupDiff::new(self)
340    }
341
342    /// Merge the changes performed on the [`PublicGroupDiff`] into this
343    /// [`PublicGroup`].
344    ///
345    /// **NOTE:** The caller must ensure that the group context in the `diff` is
346    ///           updated before calling this function with `update_group_context`.
347    pub(crate) fn merge_diff(&mut self, diff: StagedPublicGroupDiff) {
348        self.treesync.merge_diff(diff.staged_diff);
349        self.group_context = diff.group_context;
350        self.interim_transcript_hash = diff.interim_transcript_hash;
351        self.confirmation_tag = diff.confirmation_tag;
352    }
353
354    /// Derives [`EncryptionKeyPair`]s for the nodes in the shared direct path
355    /// of the leaves with index `leaf_index` and `sender_index`.  This function
356    /// also checks that the derived public keys match the existing public keys.
357    ///
358    /// Returns the [`CommitSecret`] derived from the path secret of the root
359    /// node, as well as the derived [`EncryptionKeyPair`]s. Returns an error if
360    /// the target leaf is outside of the tree.
361    ///
362    /// Returns [`DerivePathError::PublicKeyMismatch`] if the derived keys don't
363    /// match with the existing ones.
364    ///
365    /// Returns [`DerivePathError::LibraryError`] if the sender_index is not
366    /// in the tree.
367    pub(crate) fn derive_path_secrets(
368        &self,
369        crypto: &impl OpenMlsCrypto,
370        ciphersuite: Ciphersuite,
371        path_secret: PathSecret,
372        sender_index: LeafNodeIndex,
373        leaf_index: LeafNodeIndex,
374    ) -> Result<(Vec<EncryptionKeyPair>, CommitSecret), DerivePathError> {
375        self.treesync.derive_path_secrets(
376            crypto,
377            ciphersuite,
378            path_secret,
379            sender_index,
380            leaf_index,
381        )
382    }
383
384    /// Get an iterator over all [`Member`]s of this [`PublicGroup`].
385    pub fn members(&self) -> impl Iterator<Item = Member> + '_ {
386        self.treesync().full_leave_members()
387    }
388
389    /// Export the nodes of the public tree.
390    pub fn export_ratchet_tree(&self) -> RatchetTree {
391        self.treesync().export_ratchet_tree()
392    }
393
394    /// Add the [`QueuedProposal`] to the [`PublicGroup`]s internal [`ProposalStore`].
395    pub fn add_proposal<Storage: PublicStorageProvider>(
396        &mut self,
397        storage: &Storage,
398        proposal: QueuedProposal,
399    ) -> Result<(), Storage::Error> {
400        storage.queue_proposal(self.group_id(), &proposal.proposal_reference(), &proposal)?;
401        self.proposal_store.add(proposal);
402        Ok(())
403    }
404
405    /// Remove the Proposal with the given [`ProposalRef`] from the [`PublicGroup`]s internal [`ProposalStore`].
406    pub fn remove_proposal<Storage: PublicStorageProvider>(
407        &mut self,
408        storage: &Storage,
409        proposal_ref: &ProposalRef,
410    ) -> Result<(), Storage::Error> {
411        storage.remove_proposal(self.group_id(), proposal_ref)?;
412        self.proposal_store.remove(proposal_ref);
413        Ok(())
414    }
415
416    /// Return all queued proposals
417    pub fn queued_proposals<Storage: PublicStorageProvider>(
418        &self,
419        storage: &Storage,
420    ) -> Result<Vec<(ProposalRef, QueuedProposal)>, Storage::Error> {
421        storage.queued_proposals(self.group_id())
422    }
423}
424
425// Getters
426impl PublicGroup {
427    /// Get the ciphersuite.
428    pub fn ciphersuite(&self) -> Ciphersuite {
429        self.group_context.ciphersuite()
430    }
431
432    /// Get the version.
433    pub fn version(&self) -> ProtocolVersion {
434        self.group_context.protocol_version()
435    }
436
437    /// Get the group id.
438    pub fn group_id(&self) -> &GroupId {
439        self.group_context.group_id()
440    }
441
442    /// Get the group context.
443    pub fn group_context(&self) -> &GroupContext {
444        &self.group_context
445    }
446
447    /// Get the required capabilities.
448    pub fn required_capabilities(&self) -> Option<&RequiredCapabilitiesExtension> {
449        self.group_context.required_capabilities()
450    }
451
452    /// Get treesync.
453    fn treesync(&self) -> &TreeSync {
454        &self.treesync
455    }
456
457    /// Get confirmation tag.
458    pub fn confirmation_tag(&self) -> &ConfirmationTag {
459        &self.confirmation_tag
460    }
461
462    /// Return a reference to the leaf at the given `LeafNodeIndex` or `None` if the
463    /// leaf is blank.
464    pub fn leaf(&self, leaf_index: LeafNodeIndex) -> Option<&LeafNode> {
465        self.treesync().leaf(leaf_index)
466    }
467
468    /// Returns the tree size
469    pub(crate) fn tree_size(&self) -> TreeSize {
470        self.treesync().tree_size()
471    }
472
473    fn interim_transcript_hash(&self) -> &[u8] {
474        &self.interim_transcript_hash
475    }
476
477    /// Return a vector containing all [`EncryptionKey`]s for which the owner of
478    /// the given `leaf_index` should have private key material.
479    pub(crate) fn owned_encryption_keys(&self, leaf_index: LeafNodeIndex) -> Vec<EncryptionKey> {
480        self.treesync().owned_encryption_keys(leaf_index)
481    }
482
483    /// Stores the [`PublicGroup`] to storage. Called from methods creating a new group and mutating an
484    /// existing group, both inside [`PublicGroup`] and in [`MlsGroup`].
485    ///
486    /// [`MlsGroup`]: crate::group::MlsGroup
487    pub(crate) fn store<Storage: PublicStorageProvider>(
488        &self,
489        storage: &Storage,
490    ) -> Result<(), Storage::Error> {
491        let group_id = self.group_context.group_id();
492        storage.write_tree(group_id, self.treesync())?;
493        storage.write_confirmation_tag(group_id, self.confirmation_tag())?;
494        storage.write_context(group_id, self.group_context())?;
495        storage.write_interim_transcript_hash(
496            group_id,
497            &InterimTranscriptHash(self.interim_transcript_hash.clone()),
498        )?;
499        Ok(())
500    }
501
502    /// Deletes the [`PublicGroup`] from storage.
503    pub fn delete<Storage: PublicStorageProvider>(
504        storage: &Storage,
505        group_id: &GroupId,
506    ) -> Result<(), Storage::Error> {
507        storage.delete_tree(group_id)?;
508        storage.delete_confirmation_tag(group_id)?;
509        storage.delete_context(group_id)?;
510        storage.delete_interim_transcript_hash(group_id)?;
511
512        Ok(())
513    }
514
515    /// Loads the [`PublicGroup`] corresponding to a [`GroupId`] from storage.
516    pub fn load<Storage: PublicStorageProvider>(
517        storage: &Storage,
518        group_id: &GroupId,
519    ) -> Result<Option<Self>, Storage::Error> {
520        let treesync = storage.tree(group_id)?;
521        let proposals: Vec<(ProposalRef, QueuedProposal)> = storage.queued_proposals(group_id)?;
522        let group_context = storage.group_context(group_id)?;
523        let interim_transcript_hash: Option<InterimTranscriptHash> =
524            storage.interim_transcript_hash(group_id)?;
525        let confirmation_tag = storage.confirmation_tag(group_id)?;
526        let mut proposal_store = ProposalStore::new();
527
528        for (_ref, proposal) in proposals {
529            proposal_store.add(proposal);
530        }
531
532        let build = || -> Option<Self> {
533            Some(Self {
534                treesync: treesync?,
535                proposal_store,
536                group_context: group_context?,
537                interim_transcript_hash: interim_transcript_hash?.0,
538                confirmation_tag: confirmation_tag?,
539            })
540        };
541
542        Ok(build())
543    }
544
545    /// Returns a reference to the [`ProposalStore`].
546    pub(crate) fn proposal_store(&self) -> &ProposalStore {
547        &self.proposal_store
548    }
549
550    /// Returns a mutable reference to the [`ProposalStore`].
551    pub(crate) fn proposal_store_mut(&mut self) -> &mut ProposalStore {
552        &mut self.proposal_store
553    }
554}
555
556// Test functions
557#[cfg(any(feature = "test-utils", test))]
558impl PublicGroup {
559    pub(crate) fn context_mut(&mut self) -> &mut GroupContext {
560        &mut self.group_context
561    }
562
563    #[cfg(test)]
564    pub(crate) fn set_group_context(&mut self, group_context: GroupContext) {
565        self.group_context = group_context;
566    }
567
568    #[cfg(test)]
569    pub(crate) fn encrypt_path(
570        &self,
571        provider: &impl crate::storage::OpenMlsProvider,
572        ciphersuite: Ciphersuite,
573        path: &[PlainUpdatePathNode],
574        group_context: &[u8],
575        exclusion_list: &HashSet<&LeafNodeIndex>,
576        own_leaf_index: LeafNodeIndex,
577    ) -> Result<Vec<UpdatePathNode>, LibraryError> {
578        self.treesync().empty_diff().encrypt_path(
579            provider.crypto(),
580            ciphersuite,
581            path,
582            group_context,
583            exclusion_list,
584            own_leaf_index,
585        )
586    }
587}