Skip to main content

openmls/group/mls_group/
exporting.rs

1use std::{
2    fmt::{Debug, Formatter},
3    marker::PhantomData,
4};
5
6use errors::{ExportGroupInfoError, ExportSecretError};
7use openmls_traits::{crypto::OpenMlsCrypto, signatures::Signer};
8use zeroize::ZeroizeOnDrop;
9
10use crate::{
11    ciphersuite::{HpkePublicKey, Secret},
12    extensions::errors::InvalidExtensionError,
13    group::reinit::ReInitInfo,
14    schedule::{EpochAuthenticator, ResumptionPskSecret},
15};
16#[cfg(feature = "extensions-draft")]
17use crate::{
18    component::ComponentId,
19    group::{PendingSafeExportSecretError, SafeExportSecretError},
20};
21
22use super::*;
23
24/// A secret exported from a group.
25///
26/// The marker type `T` records which export function produced the secret, so
27/// secrets from different export paths cannot be confused.
28pub struct ExportedSecret<T> {
29    secret: Secret,
30    _marker: PhantomData<T>,
31}
32
33/// Marker for secrets exported via [`MlsGroup::export_secret`].
34pub struct GroupExport;
35
36/// Marker for secrets exported via [`StagedCommit::export_secret`].
37pub struct StagedCommitExport;
38
39/// Marker for secrets exported via [`StagedWelcome::export_secret`].
40pub struct StagedWelcomeExport;
41
42/// Marker for secrets exported via [`ProcessedWelcome::export_secret`].
43pub struct ProcessedWelcomeExport;
44
45/// Marker for secrets exported via [`MlsGroup::safe_export_secret`].
46#[cfg(feature = "extensions-draft")]
47pub struct GroupSafeExport;
48
49/// Marker for secrets exported via
50/// [`MlsGroup::safe_export_secret_from_pending`].
51#[cfg(feature = "extensions-draft")]
52pub struct PendingSafeExport;
53
54/// Marker for secrets exported via [`StagedCommit::safe_export_secret`] or
55/// [`ProcessedMessage::safe_export_secret`].
56///
57/// [`ProcessedMessage::safe_export_secret`]: crate::framing::ProcessedMessage::safe_export_secret
58#[cfg(feature = "extensions-draft")]
59pub struct StagedCommitSafeExport;
60
61impl<T> ExportedSecret<T> {
62    pub(crate) fn new(secret: Secret) -> Self {
63        Self {
64            secret,
65            _marker: PhantomData,
66        }
67    }
68
69    /// Returns the secret bytes.
70    pub fn as_slice(&self) -> &[u8] {
71        self.secret.as_slice()
72    }
73}
74
75#[cfg(feature = "extensions-draft")]
76impl ExportedSecret<StagedCommitSafeExport> {
77    /// Re-wraps a safe export of a staged commit as a safe export of the
78    /// pending commit, for [`MlsGroup::safe_export_secret_from_pending`].
79    pub(crate) fn into_pending_safe_export(self) -> ExportedSecret<PendingSafeExport> {
80        ExportedSecret::new(self.secret)
81    }
82}
83
84impl<T> AsRef<[u8]> for ExportedSecret<T> {
85    fn as_ref(&self) -> &[u8] {
86        self.secret.as_slice()
87    }
88}
89
90// The inner [`Secret`] is zeroized when dropped.
91impl<T> ZeroizeOnDrop for ExportedSecret<T> {}
92
93impl<T> Debug for ExportedSecret<T> {
94    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
95        f.debug_struct("ExportedSecret")
96            .field("secret", &self.secret)
97            .finish()
98    }
99}
100
101impl<T, U> PartialEq<ExportedSecret<U>> for ExportedSecret<T> {
102    // Constant time comparison.
103    fn eq(&self, other: &ExportedSecret<U>) -> bool {
104        self.secret == other.secret
105    }
106}
107
108impl MlsGroup {
109    // === Export secrets ===
110
111    /// Exports a secret from the current epoch.
112    /// Returns [`ExportSecretError::KeyLengthTooLong`] if the requested
113    /// key length is too long.
114    /// Returns [`ExportSecretError::GroupStateError(MlsGroupStateError::UseAfterEviction)`](MlsGroupStateError::UseAfterEviction)
115    /// if the group is not active.
116    pub fn export_secret<CryptoProvider: OpenMlsCrypto>(
117        &self,
118        crypto: &CryptoProvider,
119        label: &str,
120        context: &[u8],
121        key_length: usize,
122    ) -> Result<ExportedSecret<GroupExport>, ExportSecretError> {
123        if key_length > u16::MAX as usize {
124            log::error!("Got a key that is larger than u16::MAX");
125            return Err(ExportSecretError::KeyLengthTooLong);
126        }
127
128        if self.is_active() {
129            Ok(ExportedSecret::new(
130                self.group_epoch_secrets
131                    .exporter_secret()
132                    .derive_exported_secret(self.ciphersuite(), crypto, label, context, key_length)
133                    .map_err(LibraryError::unexpected_crypto_error)?,
134            ))
135        } else {
136            Err(ExportSecretError::GroupStateError(
137                MlsGroupStateError::UseAfterEviction,
138            ))
139        }
140    }
141
142    /// Export a secret from the forward secure exporter for the component with
143    /// the given component ID.
144    #[cfg(feature = "extensions-draft")]
145    pub fn safe_export_secret<Crypto: OpenMlsCrypto, Storage: StorageProvider>(
146        &mut self,
147        crypto: &Crypto,
148        storage: &Storage,
149        component_id: ComponentId,
150    ) -> Result<ExportedSecret<GroupSafeExport>, SafeExportSecretError<Storage::Error>> {
151        if !self.is_active() {
152            return Err(SafeExportSecretError::GroupState(
153                MlsGroupStateError::UseAfterEviction,
154            ));
155        }
156        let group_id = self.public_group.group_id();
157        let ciphersuite = self.ciphersuite();
158        let Some(application_export_tree) = self.application_export_tree.as_mut() else {
159            return Err(SafeExportSecretError::Unsupported);
160        };
161        let component_secret =
162            application_export_tree.safe_export_secret(crypto, ciphersuite, component_id)?;
163        storage
164            .write_application_export_tree(group_id, application_export_tree)
165            .map_err(SafeExportSecretError::Storage)?;
166
167        Ok(ExportedSecret::new(component_secret))
168    }
169
170    /// Export a secret from the forward secure exporter of the pending commit
171    /// state for the component with the given component ID.
172    #[cfg(feature = "extensions-draft")]
173    pub fn safe_export_secret_from_pending<Provider: StorageProvider>(
174        &mut self,
175        crypto: &impl OpenMlsCrypto,
176        storage: &Provider,
177        component_id: ComponentId,
178    ) -> Result<ExportedSecret<PendingSafeExport>, PendingSafeExportSecretError<Provider::Error>>
179    {
180        let group_id = self.group_id().clone();
181        let MlsGroupState::PendingCommit(ref mut group_state) = self.group_state else {
182            return Err(PendingSafeExportSecretError::NoPendingCommit);
183        };
184        let PendingCommitState::Member(ref mut staged_commit) = **group_state else {
185            return Err(PendingSafeExportSecretError::NotGroupMember);
186        };
187        let secret = staged_commit.safe_export_secret(crypto, component_id)?;
188        storage
189            .write_group_state(&group_id, &self.group_state)
190            .map_err(PendingSafeExportSecretError::Storage)?;
191        Ok(secret.into_pending_safe_export())
192    }
193
194    /// Returns the epoch authenticator of the current epoch.
195    pub fn epoch_authenticator(&self) -> &EpochAuthenticator {
196        self.group_epoch_secrets().epoch_authenticator()
197    }
198
199    /// Returns the resumption PSK secret of the current epoch.
200    pub fn resumption_psk_secret(&self) -> &ResumptionPskSecret {
201        self.group_epoch_secrets().resumption_psk()
202    }
203
204    /// Export the information a sub-group branch needs from this (parent) group,
205    /// as described in [RFC 9420 §11.3].
206    ///
207    /// Hand the resulting [`BranchInfo`] to the sender
208    /// ([`MlsGroupBuilder::branch`](crate::group::MlsGroupBuilder::branch)) and to
209    /// the receiver
210    /// ([`StagedWelcome::build_from_branch`](crate::group::StagedWelcome::build_from_branch)).
211    ///
212    /// The returned [`BranchInfo`] carries this group's resumption PSK secret,
213    /// which is sensitive key material.
214    ///
215    /// [RFC 9420 §11.3]: https://www.rfc-editor.org/rfc/rfc9420.html#name-subgroup-branching
216    pub fn branch_info(&self) -> BranchInfo {
217        BranchInfo {
218            version: self.version(),
219            ciphersuite: self.ciphersuite(),
220            group_id: self.group_id().clone(),
221            epoch: self.epoch(),
222            resumption_psk_secret: self.resumption_psk_secret().clone(),
223            member_credentials: self.members().map(|m| m.credential).collect(),
224        }
225    }
226
227    /// Export the information a reinit needs from this predecessor group,
228    /// as described in [RFC 9420 §11.2].
229    ///
230    /// Returns [`None`] if the group is active and thus cannot have processed a reinit proposal.
231    ///
232    /// `proposal` must be the ReInit proposal covered by the commit that
233    /// suspended this group.
234    ///
235    /// Hand the resulting [`ReInitInfo`] to the receiver
236    /// ([`StagedWelcome::build_from_reinit`](crate::group::StagedWelcome::build_from_reinit)).
237    /// The sender still seeds the new group from this group directly via
238    /// [`MlsGroupBuilder::reinit`](crate::group::MlsGroupBuilder::reinit).
239    ///
240    /// The returned [`ReInitInfo`] carries this group's resumption PSK secret,
241    /// which is sensitive key material.
242    ///
243    /// [RFC 9420 §11.2]: https://www.rfc-editor.org/rfc/rfc9420.html#name-reinitialization
244    pub fn reinit_info(&self, proposal: ReInitProposal) -> Option<ReInitInfo> {
245        // TODO (breaking change to storage): Introduce a dedicated state for shutdown after reinitialization.
246        // If that also includes the ReInit proposal, we can query the [`ReInitInfo`] directly from the old group
247        // without inspecting incoming commits.
248
249        if !matches!(self.group_state, MlsGroupState::Inactive) {
250            return None;
251        }
252
253        Some(ReInitInfo {
254            proposal,
255            old_group_id: self.group_id().clone(),
256            old_group_epoch: self.epoch(),
257            resumption_psk_secret: self.resumption_psk_secret().clone(),
258            member_credentials: self.members().map(|member| member.credential).collect(),
259        })
260    }
261
262    /// Returns a resumption psk for a given epoch. If no resumption psk
263    /// is available for that epoch,  `None` is returned.
264    pub fn get_past_resumption_psk(&self, epoch: GroupEpoch) -> Option<&ResumptionPskSecret> {
265        self.resumption_psk_store.get(epoch)
266    }
267
268    /// Export a group info object for this group.
269    pub fn export_group_info<CryptoProvider: OpenMlsCrypto>(
270        &self,
271        crypto: &CryptoProvider,
272        signer: &impl Signer,
273        with_ratchet_tree: bool,
274    ) -> Result<MlsMessageOut, ExportGroupInfoError> {
275        self.export_group_info_with_additional_extensions(crypto, signer, with_ratchet_tree, None)
276    }
277
278    /// Export a group info object for this group, with additional extensions.
279    ///
280    ///  Returns an error if a  [`RatchetTreeExtension`] or [`ExternalPubExtension`] is added
281    ///  directly here.
282    pub fn export_group_info_with_additional_extensions<CryptoProvider: OpenMlsCrypto>(
283        &self,
284        crypto: &CryptoProvider,
285        signer: &impl Signer,
286        with_ratchet_tree: bool,
287        additional_extensions: impl IntoIterator<Item = Extension>,
288    ) -> Result<MlsMessageOut, ExportGroupInfoError> {
289        let extensions = {
290            let ratchet_tree_extension = || {
291                Extension::RatchetTree(RatchetTreeExtension::new(
292                    self.public_group().export_ratchet_tree(),
293                ))
294            };
295
296            let external_pub_extension = || -> Result<Extension, ExportGroupInfoError> {
297                let external_pub = self
298                    .group_epoch_secrets()
299                    .external_secret()
300                    .derive_external_keypair(crypto, self.ciphersuite())
301                    .map_err(LibraryError::unexpected_crypto_error)?
302                    .public;
303                Ok(Extension::ExternalPub(ExternalPubExtension::new(
304                    HpkePublicKey::from(external_pub),
305                )))
306            };
307
308            let mut extensions = if with_ratchet_tree {
309                vec![ratchet_tree_extension(), external_pub_extension()?]
310            } else {
311                vec![external_pub_extension()?]
312            };
313
314            extensions.extend(
315                additional_extensions
316                    .into_iter()
317                    .map(|extension| {
318                        if extension.as_ratchet_tree_extension().is_ok()
319                            || extension.as_external_pub_extension().is_ok()
320                        {
321                            Err(InvalidExtensionError::CannotAddDirectlyToGroupInfo)
322                        } else {
323                            Ok(extension)
324                        }
325                    })
326                    .collect::<Result<Vec<_>, _>>()?,
327            );
328
329            Extensions::from_vec(extensions)?
330        };
331
332        // Create to-be-signed group info.
333        let group_info_tbs = GroupInfoTBS::new(
334            self.context().clone(),
335            extensions,
336            self.message_secrets()
337                .confirmation_key()
338                .tag(
339                    crypto,
340                    self.ciphersuite(),
341                    self.context().confirmed_transcript_hash(),
342                )
343                .map_err(LibraryError::unexpected_crypto_error)?,
344            self.own_leaf_index(),
345        )?;
346
347        // Sign to-be-signed group info.
348        let group_info = group_info_tbs
349            .sign(signer)
350            .map_err(|_| LibraryError::custom("Signing failed"))?;
351        Ok(group_info.into())
352    }
353}