Skip to main content

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