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