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    #[openmls_traits::maybe_async]
146    pub async fn safe_export_secret<Crypto: OpenMlsCrypto, Storage: StorageProvider>(
147        &mut self,
148        crypto: &Crypto,
149        storage: &Storage,
150        component_id: ComponentId,
151    ) -> Result<ExportedSecret<GroupSafeExport>, SafeExportSecretError<Storage::Error>> {
152        if !self.is_active() {
153            return Err(SafeExportSecretError::GroupState(
154                MlsGroupStateError::UseAfterEviction,
155            ));
156        }
157        let group_id = self.public_group.group_id();
158        let ciphersuite = self.ciphersuite();
159        let Some(application_export_tree) = self.application_export_tree.as_mut() else {
160            return Err(SafeExportSecretError::Unsupported);
161        };
162        let component_secret =
163            application_export_tree.safe_export_secret(crypto, ciphersuite, component_id)?;
164        storage
165            .write_application_export_tree(group_id, application_export_tree)
166            .await
167            .map_err(SafeExportSecretError::Storage)?;
168
169        Ok(ExportedSecret::new(component_secret))
170    }
171
172    /// Export a secret from the forward secure exporter of the pending commit
173    /// state for the component with the given component ID.
174    #[cfg(feature = "extensions-draft")]
175    #[openmls_traits::maybe_async]
176    pub async fn safe_export_secret_from_pending<Provider: StorageProvider>(
177        &mut self,
178        crypto: &impl OpenMlsCrypto,
179        storage: &Provider,
180        component_id: ComponentId,
181    ) -> Result<ExportedSecret<PendingSafeExport>, PendingSafeExportSecretError<Provider::Error>>
182    {
183        let group_id = self.group_id().clone();
184        let MlsGroupState::PendingCommit(ref mut group_state) = self.group_state else {
185            return Err(PendingSafeExportSecretError::NoPendingCommit);
186        };
187        let PendingCommitState::Member(ref mut staged_commit) = **group_state else {
188            return Err(PendingSafeExportSecretError::NotGroupMember);
189        };
190        let secret = staged_commit.safe_export_secret(crypto, component_id)?;
191        storage
192            .write_group_state(&group_id, &self.group_state)
193            .await
194            .map_err(PendingSafeExportSecretError::Storage)?;
195        Ok(secret.into_pending_safe_export())
196    }
197
198    /// Returns the epoch authenticator of the current epoch.
199    pub fn epoch_authenticator(&self) -> &EpochAuthenticator {
200        self.group_epoch_secrets().epoch_authenticator()
201    }
202
203    /// Returns the resumption PSK secret of the current epoch.
204    pub fn resumption_psk_secret(&self) -> &ResumptionPskSecret {
205        self.group_epoch_secrets().resumption_psk()
206    }
207
208    /// Export the information a sub-group branch needs from this (parent) group,
209    /// as described in [RFC 9420 §11.3].
210    ///
211    /// Hand the resulting [`BranchInfo`] to the sender
212    /// ([`MlsGroupBuilder::branch`](crate::group::MlsGroupBuilder::branch)) and to
213    /// the receiver
214    /// ([`StagedWelcome::build_from_branch`](crate::group::StagedWelcome::build_from_branch)).
215    ///
216    /// The returned [`BranchInfo`] carries this group's resumption PSK secret,
217    /// which is sensitive key material.
218    ///
219    /// [RFC 9420 §11.3]: https://www.rfc-editor.org/rfc/rfc9420.html#name-subgroup-branching
220    pub fn branch_info(&self) -> BranchInfo {
221        BranchInfo {
222            version: self.version(),
223            ciphersuite: self.ciphersuite(),
224            group_id: self.group_id().clone(),
225            epoch: self.epoch(),
226            resumption_psk_secret: self.resumption_psk_secret().clone(),
227            member_credentials: self.members().map(|m| m.credential).collect(),
228        }
229    }
230
231    /// Export the information a reinit needs from this predecessor group,
232    /// as described in [RFC 9420 §11.2].
233    ///
234    /// Returns [`None`] if the group is active and thus cannot have processed a reinit proposal.
235    ///
236    /// `proposal` must be the ReInit proposal covered by the commit that
237    /// suspended this group.
238    ///
239    /// Hand the resulting [`ReInitInfo`] to the receiver
240    /// ([`StagedWelcome::build_from_reinit`](crate::group::StagedWelcome::build_from_reinit)).
241    /// The sender still seeds the new group from this group directly via
242    /// [`MlsGroupBuilder::reinit`](crate::group::MlsGroupBuilder::reinit).
243    ///
244    /// The returned [`ReInitInfo`] carries this group's resumption PSK secret,
245    /// which is sensitive key material.
246    ///
247    /// [RFC 9420 §11.2]: https://www.rfc-editor.org/rfc/rfc9420.html#name-reinitialization
248    pub fn reinit_info(&self, proposal: ReInitProposal) -> Option<ReInitInfo> {
249        // TODO (breaking change to storage): Introduce a dedicated state for shutdown after reinitialization.
250        // If that also includes the ReInit proposal, we can query the [`ReInitInfo`] directly from the old group
251        // without inspecting incoming commits.
252
253        if !matches!(self.group_state, MlsGroupState::Inactive) {
254            return None;
255        }
256
257        Some(ReInitInfo {
258            proposal,
259            old_group_id: self.group_id().clone(),
260            old_group_epoch: self.epoch(),
261            resumption_psk_secret: self.resumption_psk_secret().clone(),
262            member_credentials: self.members().map(|member| member.credential).collect(),
263        })
264    }
265
266    /// Returns a resumption psk for a given epoch. If no resumption psk
267    /// is available for that epoch,  `None` is returned.
268    pub fn get_past_resumption_psk(&self, epoch: GroupEpoch) -> Option<&ResumptionPskSecret> {
269        self.resumption_psk_store.get(epoch)
270    }
271
272    /// Export a group info object for this group.
273    pub fn export_group_info<CryptoProvider: OpenMlsCrypto>(
274        &self,
275        crypto: &CryptoProvider,
276        signer: &impl Signer,
277        with_ratchet_tree: bool,
278    ) -> Result<MlsMessageOut, ExportGroupInfoError> {
279        self.export_group_info_with_additional_extensions(crypto, signer, with_ratchet_tree, None)
280    }
281
282    /// Export a group info object for this group, with additional extensions.
283    ///
284    ///  Returns an error if a  [`RatchetTreeExtension`] or [`ExternalPubExtension`] is added
285    ///  directly here.
286    pub fn export_group_info_with_additional_extensions<CryptoProvider: OpenMlsCrypto>(
287        &self,
288        crypto: &CryptoProvider,
289        signer: &impl Signer,
290        with_ratchet_tree: bool,
291        additional_extensions: impl IntoIterator<Item = Extension>,
292    ) -> Result<MlsMessageOut, ExportGroupInfoError> {
293        let extensions = {
294            let ratchet_tree_extension = || {
295                Extension::RatchetTree(RatchetTreeExtension::new(
296                    self.public_group().export_ratchet_tree(),
297                ))
298            };
299
300            let external_pub_extension = || -> Result<Extension, ExportGroupInfoError> {
301                let external_pub = self
302                    .group_epoch_secrets()
303                    .external_secret()
304                    .derive_external_keypair(crypto, self.ciphersuite())
305                    .map_err(LibraryError::unexpected_crypto_error)?
306                    .public;
307                Ok(Extension::ExternalPub(ExternalPubExtension::new(
308                    HpkePublicKey::from(external_pub),
309                )))
310            };
311
312            let mut extensions = if with_ratchet_tree {
313                vec![ratchet_tree_extension(), external_pub_extension()?]
314            } else {
315                vec![external_pub_extension()?]
316            };
317
318            extensions.extend(
319                additional_extensions
320                    .into_iter()
321                    .map(|extension| {
322                        if extension.as_ratchet_tree_extension().is_ok()
323                            || extension.as_external_pub_extension().is_ok()
324                        {
325                            Err(InvalidExtensionError::CannotAddDirectlyToGroupInfo)
326                        } else {
327                            Ok(extension)
328                        }
329                    })
330                    .collect::<Result<Vec<_>, _>>()?,
331            );
332
333            Extensions::from_vec(extensions)?
334        };
335
336        // Create to-be-signed group info.
337        let group_info_tbs = GroupInfoTBS::new(
338            self.context().clone(),
339            extensions,
340            self.message_secrets()
341                .confirmation_key()
342                .tag(
343                    crypto,
344                    self.ciphersuite(),
345                    self.context().confirmed_transcript_hash(),
346                )
347                .map_err(LibraryError::unexpected_crypto_error)?,
348            self.own_leaf_index(),
349        )?;
350
351        // Sign to-be-signed group info.
352        let group_info = group_info_tbs
353            .sign(signer)
354            .map_err(|_| LibraryError::custom("Signing failed"))?;
355        Ok(group_info.into())
356    }
357}