openmls/group/mls_group/
creation.rs

1use errors::NewGroupError;
2use openmls_traits::{signatures::Signer, storage::StorageProvider as StorageProviderTrait};
3
4use super::{builder::MlsGroupBuilder, *};
5use crate::{
6    credentials::CredentialWithKey,
7    group::errors::{ExternalCommitError, WelcomeError},
8    messages::{
9        group_info::{GroupInfo, VerifiableGroupInfo},
10        Welcome,
11    },
12    schedule::{
13        psk::{store::ResumptionPskStore, PreSharedKeyId},
14        EpochSecrets, InitSecret,
15    },
16    storage::OpenMlsProvider,
17    treesync::{
18        errors::{DerivePathError, PublicTreeError},
19        node::leaf_node::{Capabilities, LeafNodeParameters},
20        RatchetTreeIn,
21    },
22};
23
24#[cfg(doc)]
25use crate::key_packages::KeyPackage;
26
27impl MlsGroup {
28    // === Group creation ===
29
30    /// Creates a builder which can be used to configure and build
31    /// a new [`MlsGroup`].
32    pub fn builder() -> MlsGroupBuilder {
33        MlsGroupBuilder::new()
34    }
35
36    /// Creates a new group with the creator as the only member (and a random
37    /// group ID).
38    ///
39    /// This function removes the private key corresponding to the
40    /// `key_package` from the key store.
41    pub fn new<Provider: OpenMlsProvider>(
42        provider: &Provider,
43        signer: &impl Signer,
44        mls_group_create_config: &MlsGroupCreateConfig,
45        credential_with_key: CredentialWithKey,
46    ) -> Result<Self, NewGroupError<Provider::StorageError>> {
47        MlsGroupBuilder::new().build_internal(
48            provider,
49            signer,
50            credential_with_key,
51            Some(mls_group_create_config.clone()),
52        )
53    }
54
55    /// Creates a new group with a given group ID with the creator as the only
56    /// member.
57    pub fn new_with_group_id<Provider: OpenMlsProvider>(
58        provider: &Provider,
59        signer: &impl Signer,
60        mls_group_create_config: &MlsGroupCreateConfig,
61        group_id: GroupId,
62        credential_with_key: CredentialWithKey,
63    ) -> Result<Self, NewGroupError<Provider::StorageError>> {
64        MlsGroupBuilder::new()
65            .with_group_id(group_id)
66            .build_internal(
67                provider,
68                signer,
69                credential_with_key,
70                Some(mls_group_create_config.clone()),
71            )
72    }
73
74    /// Join an existing group through an External Commit.
75    /// The resulting [`MlsGroup`] instance starts off with a pending
76    /// commit (the external commit, which adds this client to the group).
77    /// Merging this commit is necessary for this [`MlsGroup`] instance to
78    /// function properly, as, for example, this client is not yet part of the
79    /// tree. As a result, it is not possible to clear the pending commit. If
80    /// the external commit was rejected due to an epoch change, the
81    /// [`MlsGroup`] instance has to be discarded and a new one has to be
82    /// created using this function based on the latest `ratchet_tree` and
83    /// group info. For more information on the external init process,
84    /// please see Section 11.2.1 in the MLS specification.
85    ///
86    /// Note: If there is a group member in the group with the same identity as
87    /// us, this will create a remove proposal.
88    #[allow(clippy::too_many_arguments)]
89    pub fn join_by_external_commit<Provider: OpenMlsProvider>(
90        provider: &Provider,
91        signer: &impl Signer,
92        ratchet_tree: Option<RatchetTreeIn>,
93        verifiable_group_info: VerifiableGroupInfo,
94        mls_group_config: &MlsGroupJoinConfig,
95        capabilities: Option<Capabilities>,
96        extensions: Option<Extensions>,
97        aad: &[u8],
98        credential_with_key: CredentialWithKey,
99    ) -> Result<(Self, MlsMessageOut, Option<GroupInfo>), ExternalCommitError<Provider::StorageError>>
100    {
101        // Prepare the commit parameters
102        let framing_parameters = FramingParameters::new(aad, WireFormat::PublicMessage);
103
104        let leaf_node_parameters = LeafNodeParameters::builder()
105            .with_capabilities(capabilities.unwrap_or_default())
106            .with_extensions(extensions.unwrap_or_default())
107            .build();
108        let mut params = CreateCommitParams::builder()
109            .external_commit(credential_with_key, framing_parameters)
110            .leaf_node_parameters(leaf_node_parameters)
111            .build();
112
113        // Build the ratchet tree
114
115        // Set nodes either from the extension or from the `nodes_option`.
116        // If we got a ratchet tree extension in the welcome, we enable it for
117        // this group. Note that this is not strictly necessary. But there's
118        // currently no other mechanism to enable the extension.
119        let ratchet_tree = match verifiable_group_info.extensions().ratchet_tree() {
120            Some(extension) => extension.ratchet_tree().clone(),
121            None => match ratchet_tree {
122                Some(ratchet_tree) => ratchet_tree,
123                None => return Err(ExternalCommitError::MissingRatchetTree),
124            },
125        };
126
127        let (public_group, group_info) = PublicGroup::from_external_internal(
128            provider.crypto(),
129            ratchet_tree,
130            verifiable_group_info,
131            // Existing proposals are discarded when joining by external commit.
132            ProposalStore::new(),
133        )?;
134        let group_context = public_group.group_context();
135
136        // Obtain external_pub from GroupInfo extensions.
137        let external_pub = group_info
138            .extensions()
139            .external_pub()
140            .ok_or(ExternalCommitError::MissingExternalPub)?
141            .external_pub();
142
143        let (init_secret, kem_output) = InitSecret::from_group_context(
144            provider.crypto(),
145            group_context,
146            external_pub.as_slice(),
147        )
148        .map_err(|_| ExternalCommitError::UnsupportedCiphersuite)?;
149
150        // The `EpochSecrets` we create here are essentially zero, with the
151        // exception of the `InitSecret`, which is all we need here for the
152        // external commit.
153        let epoch_secrets = EpochSecrets::with_init_secret(
154            provider.crypto(),
155            group_info.group_context().ciphersuite(),
156            init_secret,
157        )
158        .map_err(LibraryError::unexpected_crypto_error)?;
159        let (group_epoch_secrets, message_secrets) = epoch_secrets.split_secrets(
160            group_context
161                .tls_serialize_detached()
162                .map_err(LibraryError::missing_bound_check)?,
163            public_group.tree_size(),
164            // We use a fake own index of 0 here, as we're not going to use the
165            // tree for encryption until after the first commit. This issue is
166            // tracked in #767.
167            LeafNodeIndex::new(0u32),
168        );
169        let message_secrets_store = MessageSecretsStore::new_with_secret(0, message_secrets);
170
171        let external_init_proposal = Proposal::ExternalInit(ExternalInitProposal::from(kem_output));
172
173        let mut inline_proposals = vec![external_init_proposal];
174
175        // If there is a group member in the group with the same identity as us,
176        // commit a remove proposal.
177        let signature_key = params.credential_with_key().signature_key.as_slice();
178        if let Some(us) = public_group
179            .members()
180            .find(|member| member.signature_key == signature_key)
181        {
182            let remove_proposal = Proposal::Remove(RemoveProposal { removed: us.index });
183            inline_proposals.push(remove_proposal);
184        };
185
186        let own_leaf_index = public_group.leftmost_free_index(inline_proposals.iter().map(Some))?;
187        params.set_inline_proposals(inline_proposals);
188
189        let mut mls_group = MlsGroup {
190            mls_group_config: mls_group_config.clone(),
191            own_leaf_nodes: vec![],
192            aad: vec![],
193            group_state: MlsGroupState::Operational,
194            public_group,
195            group_epoch_secrets,
196            own_leaf_index,
197            message_secrets_store,
198            resumption_psk_store: ResumptionPskStore::new(32),
199        };
200
201        mls_group.set_max_past_epochs(mls_group_config.max_past_epochs);
202
203        // Immediately create the commit to add ourselves to the group.
204        let create_commit_result = mls_group
205            .create_external_commit(params, provider, signer)
206            .map_err(|_| ExternalCommitError::CommitError)?;
207
208        mls_group.group_state = MlsGroupState::PendingCommit(Box::new(
209            PendingCommitState::External(create_commit_result.staged_commit),
210        ));
211
212        mls_group
213            .store(provider.storage())
214            .map_err(ExternalCommitError::StorageError)?;
215
216        let public_message: PublicMessage = create_commit_result.commit.into();
217
218        Ok((
219            mls_group,
220            public_message.into(),
221            create_commit_result.group_info,
222        ))
223    }
224}
225
226impl ProcessedWelcome {
227    /// Creates a new processed [`Welcome`] message , which can be
228    /// inspected before creating a [`StagedWelcome`].
229    ///
230    /// This does not require a ratchet tree yet.
231    ///
232    /// [`Welcome`]: crate::messages::Welcome
233    pub fn new_from_welcome<Provider: OpenMlsProvider>(
234        provider: &Provider,
235        mls_group_config: &MlsGroupJoinConfig,
236        welcome: Welcome,
237    ) -> Result<Self, WelcomeError<Provider::StorageError>> {
238        let (resumption_psk_store, key_package_bundle) =
239            keys_for_welcome(mls_group_config, &welcome, provider)?;
240
241        let ciphersuite = welcome.ciphersuite();
242        let Some(egs) = welcome.find_encrypted_group_secret(
243            key_package_bundle
244                .key_package()
245                .hash_ref(provider.crypto())?,
246        ) else {
247            return Err(WelcomeError::JoinerSecretNotFound);
248        };
249
250        // This check seems to be superfluous from the perspective of the RFC, but still doesn't
251        // seem like a bad idea.
252        if welcome.ciphersuite() != key_package_bundle.key_package().ciphersuite() {
253            let e = WelcomeError::CiphersuiteMismatch;
254            log::debug!("new_from_welcome {:?}", e);
255            return Err(e);
256        }
257
258        let group_secrets = GroupSecrets::try_from_ciphertext(
259            key_package_bundle.init_private_key(),
260            egs.encrypted_group_secrets(),
261            welcome.encrypted_group_info(),
262            ciphersuite,
263            provider.crypto(),
264        )?;
265        let psk_secret = {
266            let psks = load_psks(
267                provider.storage(),
268                &resumption_psk_store,
269                &group_secrets.psks,
270            )?;
271
272            PskSecret::new(provider.crypto(), ciphersuite, psks)?
273        };
274        let key_schedule = KeySchedule::init(
275            ciphersuite,
276            provider.crypto(),
277            &group_secrets.joiner_secret,
278            psk_secret,
279        )?;
280        let (welcome_key, welcome_nonce) = key_schedule
281            .welcome(provider.crypto(), ciphersuite)
282            .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?
283            .derive_welcome_key_nonce(provider.crypto(), ciphersuite)
284            .map_err(LibraryError::unexpected_crypto_error)?;
285        let verifiable_group_info = VerifiableGroupInfo::try_from_ciphertext(
286            &welcome_key,
287            &welcome_nonce,
288            welcome.encrypted_group_info(),
289            &[],
290            provider.crypto(),
291        )?;
292        if let Some(required_capabilities) =
293            verifiable_group_info.extensions().required_capabilities()
294        {
295            // Also check that our key package actually supports the extensions.
296            // As per the spec, the sender must have checked this. But you never know.
297            key_package_bundle
298                .key_package()
299                .leaf_node()
300                .capabilities()
301                .supports_required_capabilities(required_capabilities)?;
302        }
303
304        // https://validation.openmls.tech/#valn1404
305        // Verify that the cipher_suite in the GroupInfo matches the cipher_suite in the
306        // KeyPackage.
307        if verifiable_group_info.ciphersuite() != key_package_bundle.key_package().ciphersuite() {
308            let e = WelcomeError::CiphersuiteMismatch;
309            log::debug!("new_from_welcome {:?}", e);
310            return Err(e);
311        }
312
313        Ok(Self {
314            mls_group_config: mls_group_config.clone(),
315            ciphersuite,
316            group_secrets,
317            key_schedule,
318            verifiable_group_info,
319            resumption_psk_store,
320            key_package_bundle,
321        })
322    }
323
324    /// Get a reference to the GroupInfo in this Welcome message.
325    ///
326    /// **NOTE:** The group info contains **unverified** values. Use with caution.
327    pub fn unverified_group_info(&self) -> &VerifiableGroupInfo {
328        &self.verifiable_group_info
329    }
330
331    /// Get a reference to the PSKs in this Welcome message.
332    ///
333    /// **NOTE:** The group info contains **unverified** values. Use with caution.
334    pub fn psks(&self) -> &[PreSharedKeyId] {
335        &self.group_secrets.psks
336    }
337
338    /// Consume the `ProcessedWelcome` and combine it witht he ratchet tree into
339    /// a `StagedWelcome`.
340    pub fn into_staged_welcome<Provider: OpenMlsProvider>(
341        mut self,
342        provider: &Provider,
343        ratchet_tree: Option<RatchetTreeIn>,
344    ) -> Result<StagedWelcome, WelcomeError<Provider::StorageError>> {
345        // Build the ratchet tree and group
346
347        // Set nodes either from the extension or from the `nodes_option`.
348        // If we got a ratchet tree extension in the welcome, we enable it for
349        // this group. Note that this is not strictly necessary. But there's
350        // currently no other mechanism to enable the extension.
351        let ratchet_tree = match self.verifiable_group_info.extensions().ratchet_tree() {
352            Some(extension) => extension.ratchet_tree().clone(),
353            None => match ratchet_tree {
354                Some(ratchet_tree) => ratchet_tree,
355                None => return Err(WelcomeError::MissingRatchetTree),
356            },
357        };
358
359        // Since there is currently only the external pub extension, there is no
360        // group info extension of interest here.
361        let (public_group, _group_info_extensions) = PublicGroup::from_external_internal(
362            provider.crypto(),
363            ratchet_tree,
364            self.verifiable_group_info.clone(),
365            ProposalStore::new(),
366        )?;
367
368        // Find our own leaf in the tree.
369        let own_leaf_index = public_group
370            .members()
371            .find_map(|m| {
372                if m.signature_key
373                    == self
374                        .key_package_bundle
375                        .key_package()
376                        .leaf_node()
377                        .signature_key()
378                        .as_slice()
379                {
380                    Some(m.index)
381                } else {
382                    None
383                }
384            })
385            .ok_or(WelcomeError::PublicTreeError(
386                PublicTreeError::MalformedTree,
387            ))?;
388
389        let (group_epoch_secrets, message_secrets) = {
390            let serialized_group_context = public_group
391                .group_context()
392                .tls_serialize_detached()
393                .map_err(LibraryError::missing_bound_check)?;
394
395            // TODO #751: Implement PSK
396            self.key_schedule
397                .add_context(provider.crypto(), &serialized_group_context)
398                .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
399
400            let epoch_secrets = self
401                .key_schedule
402                .epoch_secrets(provider.crypto(), self.ciphersuite)
403                .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
404
405            epoch_secrets.split_secrets(
406                serialized_group_context,
407                public_group.tree_size(),
408                own_leaf_index,
409            )
410        };
411
412        let confirmation_tag = message_secrets
413            .confirmation_key()
414            .tag(
415                provider.crypto(),
416                self.ciphersuite,
417                public_group.group_context().confirmed_transcript_hash(),
418            )
419            .map_err(LibraryError::unexpected_crypto_error)?;
420
421        // Verify confirmation tag
422        // https://validation.openmls.tech/#valn1410
423        if &confirmation_tag != public_group.confirmation_tag() {
424            log::error!("Confirmation tag mismatch");
425            log_crypto!(trace, "  Got:      {:x?}", confirmation_tag);
426            log_crypto!(trace, "  Expected: {:x?}", public_group.confirmation_tag());
427            debug_assert!(false, "Confirmation tag mismatch");
428
429            // in some tests we need to be able to proceed despite the tag being wrong,
430            // e.g. to test whether a later validation check is performed correctly.
431            if !crate::skip_validation::is_disabled::confirmation_tag() {
432                return Err(WelcomeError::ConfirmationTagMismatch);
433            }
434        }
435
436        let message_secrets_store = MessageSecretsStore::new_with_secret(0, message_secrets);
437
438        // Extract and store the resumption PSK for the current epoch.
439        let resumption_psk = group_epoch_secrets.resumption_psk();
440        self.resumption_psk_store
441            .add(public_group.group_context().epoch(), resumption_psk.clone());
442
443        let welcome_sender_index = self.verifiable_group_info.signer();
444        let path_keypairs = if let Some(path_secret) = self.group_secrets.path_secret {
445            let (path_keypairs, _commit_secret) = public_group
446                .derive_path_secrets(
447                    provider.crypto(),
448                    self.ciphersuite,
449                    path_secret,
450                    welcome_sender_index,
451                    own_leaf_index,
452                )
453                .map_err(|e| match e {
454                    DerivePathError::LibraryError(e) => e.into(),
455                    DerivePathError::PublicKeyMismatch => {
456                        WelcomeError::PublicTreeError(PublicTreeError::PublicKeyMismatch)
457                    }
458                })?;
459            Some(path_keypairs)
460        } else {
461            None
462        };
463
464        let staged_welcome = StagedWelcome {
465            mls_group_config: self.mls_group_config,
466            public_group,
467            group_epoch_secrets,
468            own_leaf_index,
469            message_secrets_store,
470            resumption_psk_store: self.resumption_psk_store,
471            verifiable_group_info: self.verifiable_group_info,
472            key_package_bundle: self.key_package_bundle,
473            path_keypairs,
474        };
475
476        Ok(staged_welcome)
477    }
478}
479
480impl StagedWelcome {
481    /// Creates a new staged welcome from a [`Welcome`] message. Returns an error
482    /// ([`WelcomeError::NoMatchingKeyPackage`]) if no [`KeyPackage`]
483    /// can be found.
484    /// Note: calling this function will consume the key material for decrypting the [`Welcome`]
485    /// message, even if the caller does not turn the [`StagedWelcome`] into an [`MlsGroup`].
486    ///
487    /// [`Welcome`]: crate::messages::Welcome
488    pub fn new_from_welcome<Provider: OpenMlsProvider>(
489        provider: &Provider,
490        mls_group_config: &MlsGroupJoinConfig,
491        welcome: Welcome,
492        ratchet_tree: Option<RatchetTreeIn>,
493    ) -> Result<Self, WelcomeError<Provider::StorageError>> {
494        let processed_welcome =
495            ProcessedWelcome::new_from_welcome(provider, mls_group_config, welcome)?;
496
497        processed_welcome.into_staged_welcome(provider, ratchet_tree)
498    }
499
500    /// Returns the [`LeafNodeIndex`] of the group member that authored the [`Welcome`] message.
501    ///
502    /// [`Welcome`]: crate::messages::Welcome
503    pub fn welcome_sender_index(&self) -> LeafNodeIndex {
504        self.verifiable_group_info.signer()
505    }
506
507    /// Returns the [`LeafNode`] of the group member that authored the [`Welcome`] message.
508    ///
509    /// [`Welcome`]: crate::messages::Welcome
510    pub fn welcome_sender(&self) -> Result<&LeafNode, LibraryError> {
511        let sender_index = self.welcome_sender_index();
512        self.public_group
513            .leaf(sender_index)
514            .ok_or(LibraryError::custom(
515                "no leaf with given welcome sender index exists",
516            ))
517    }
518
519    /// Get the [`GroupContext`] of this welcome's [`PublicGroup`].
520    pub fn group_context(&self) -> &GroupContext {
521        self.public_group.group_context()
522    }
523
524    /// Get an iterator over all [`Member`]s of this welcome's [`PublicGroup`].
525    pub fn members(&self) -> impl Iterator<Item = Member> + '_ {
526        self.public_group.members()
527    }
528
529    /// Consumes the [`StagedWelcome`] and returns the respective [`MlsGroup`].
530    pub fn into_group<Provider: OpenMlsProvider>(
531        self,
532        provider: &Provider,
533    ) -> Result<MlsGroup, WelcomeError<Provider::StorageError>> {
534        // If we got a path secret, derive the path (which also checks if the
535        // public keys match) and store the derived keys in the key store.
536        let group_keypairs = if let Some(path_keypairs) = self.path_keypairs {
537            let mut keypairs = vec![self.key_package_bundle.encryption_key_pair()];
538            keypairs.extend_from_slice(&path_keypairs);
539            keypairs
540        } else {
541            vec![self.key_package_bundle.encryption_key_pair()]
542        };
543
544        let mut mls_group = MlsGroup {
545            mls_group_config: self.mls_group_config,
546            own_leaf_nodes: vec![],
547            aad: vec![],
548            group_state: MlsGroupState::Operational,
549            public_group: self.public_group,
550            group_epoch_secrets: self.group_epoch_secrets,
551            own_leaf_index: self.own_leaf_index,
552            message_secrets_store: self.message_secrets_store,
553            resumption_psk_store: self.resumption_psk_store,
554        };
555
556        mls_group
557            .store_epoch_keypairs(provider.storage(), group_keypairs.as_slice())
558            .map_err(WelcomeError::StorageError)?;
559        mls_group.set_max_past_epochs(mls_group.mls_group_config.max_past_epochs);
560
561        mls_group
562            .store(provider.storage())
563            .map_err(WelcomeError::StorageError)?;
564
565        Ok(mls_group)
566    }
567}
568
569fn keys_for_welcome<Provider: OpenMlsProvider>(
570    mls_group_config: &MlsGroupJoinConfig,
571    welcome: &Welcome,
572    provider: &Provider,
573) -> Result<
574    (ResumptionPskStore, KeyPackageBundle),
575    WelcomeError<<Provider as OpenMlsProvider>::StorageError>,
576> {
577    let resumption_psk_store = ResumptionPskStore::new(mls_group_config.number_of_resumption_psks);
578    let key_package_bundle: KeyPackageBundle = welcome
579        .secrets()
580        .iter()
581        .find_map(|egs| {
582            let hash_ref = egs.new_member();
583
584            provider
585                .storage()
586                .key_package(&hash_ref)
587                .map_err(WelcomeError::StorageError)
588                .transpose()
589        })
590        .ok_or(WelcomeError::NoMatchingKeyPackage)??;
591    if !key_package_bundle.key_package().last_resort() {
592        provider
593            .storage()
594            .delete_key_package(&key_package_bundle.key_package.hash_ref(provider.crypto())?)
595            .map_err(WelcomeError::StorageError)?;
596    } else {
597        log::debug!("Key package has last resort extension, not deleting");
598    }
599    Ok((resumption_psk_store, key_package_bundle))
600}