Skip to main content

openmls/group/mls_group/
exporting.rs

1use errors::{ExportGroupInfoError, ExportSecretError};
2use openmls_traits::{crypto::OpenMlsCrypto, signatures::Signer};
3
4use crate::{
5    ciphersuite::HpkePublicKey,
6    extensions::errors::InvalidExtensionError,
7    schedule::{EpochAuthenticator, ResumptionPskSecret},
8};
9#[cfg(feature = "extensions-draft")]
10use crate::{
11    component::ComponentId,
12    group::{PendingSafeExportSecretError, SafeExportSecretError},
13};
14
15#[cfg(feature = "virtual-clients-draft")]
16use crate::{
17    components::vc_derivation_info::{
18        EmulationEpochState, EmulatorEpochSecret, EpochId, RegisteredVcEmulationEpoch,
19        VC_COMPONENT_ID,
20    },
21    components::vc_operation_tree::OperationSecretTree,
22    group::mls_group::errors::RegisterVcEmulationEpochError,
23};
24
25use super::*;
26
27impl MlsGroup {
28    // === Export secrets ===
29
30    /// Exports a secret from the current epoch.
31    /// Returns [`ExportSecretError::KeyLengthTooLong`] if the requested
32    /// key length is too long.
33    /// Returns [`ExportSecretError::GroupStateError(MlsGroupStateError::UseAfterEviction)`](MlsGroupStateError::UseAfterEviction)
34    /// if the group is not active.
35    pub fn export_secret<CryptoProvider: OpenMlsCrypto>(
36        &self,
37        crypto: &CryptoProvider,
38        label: &str,
39        context: &[u8],
40        key_length: usize,
41    ) -> Result<Vec<u8>, ExportSecretError> {
42        if key_length > u16::MAX as usize {
43            log::error!("Got a key that is larger than u16::MAX");
44            return Err(ExportSecretError::KeyLengthTooLong);
45        }
46
47        if self.is_active() {
48            Ok(self
49                .group_epoch_secrets
50                .exporter_secret()
51                .derive_exported_secret(self.ciphersuite(), crypto, label, context, key_length)
52                .map_err(LibraryError::unexpected_crypto_error)?)
53        } else {
54            Err(ExportSecretError::GroupStateError(
55                MlsGroupStateError::UseAfterEviction,
56            ))
57        }
58    }
59
60    /// Export a secret from the forward secure exporter for the component with
61    /// the given component ID.
62    #[cfg(feature = "extensions-draft")]
63    pub fn safe_export_secret<Crypto: OpenMlsCrypto, Storage: StorageProvider>(
64        &mut self,
65        crypto: &Crypto,
66        storage: &Storage,
67        component_id: ComponentId,
68    ) -> Result<Vec<u8>, SafeExportSecretError<Storage::Error>> {
69        if !self.is_active() {
70            return Err(SafeExportSecretError::GroupState(
71                MlsGroupStateError::UseAfterEviction,
72            ));
73        }
74        let group_id = self.public_group.group_id();
75        let ciphersuite = self.ciphersuite();
76        let Some(application_export_tree) = self.application_export_tree.as_mut() else {
77            return Err(SafeExportSecretError::Unsupported);
78        };
79        let component_secret =
80            application_export_tree.safe_export_secret(crypto, ciphersuite, component_id)?;
81        storage
82            .write_application_export_tree(group_id, application_export_tree)
83            .map_err(SafeExportSecretError::Storage)?;
84
85        Ok(component_secret.as_slice().to_vec())
86    }
87
88    /// Export a secret from the forward secure exporter of the pending commit
89    /// state for the component with the given component ID.
90    #[cfg(feature = "extensions-draft")]
91    pub fn safe_export_secret_from_pending<Provider: StorageProvider>(
92        &mut self,
93        crypto: &impl OpenMlsCrypto,
94        storage: &Provider,
95        component_id: ComponentId,
96    ) -> Result<Vec<u8>, PendingSafeExportSecretError<Provider::Error>> {
97        let group_id = self.group_id().clone();
98        let MlsGroupState::PendingCommit(ref mut group_state) = self.group_state else {
99            return Err(PendingSafeExportSecretError::NoPendingCommit);
100        };
101        let PendingCommitState::Member(ref mut staged_commit) = **group_state else {
102            return Err(PendingSafeExportSecretError::NotGroupMember);
103        };
104        let secret = staged_commit.safe_export_secret(crypto, component_id)?;
105        storage
106            .write_group_state(&group_id, &self.group_state)
107            .map_err(PendingSafeExportSecretError::Storage)?;
108        Ok(secret.as_slice().to_vec())
109    }
110
111    /// Register a new virtual-clients emulation epoch for this *emulation*
112    /// group.
113    ///
114    /// Sources the per-emulation-epoch root secret from
115    /// `self.safe_export_secret(crypto, storage, VC_COMPONENT_ID)`,
116    /// derives the [`EpochId`], the AEAD key, and the epoch base secret,
117    /// builds the per-epoch operation secret tree (sized like the emulation
118    /// group's ratchet tree), and persists the tree and the per-epoch state
119    /// in the storage provider keyed on the derived `EpochId`. Returns the
120    /// `EpochId` so the caller can reference this emulation epoch on
121    /// subsequent virtual-clients commits.
122    ///
123    /// Idempotent per *group epoch*: the registration is recorded keyed on the
124    /// group id, and a repeated call in the same group epoch returns the
125    /// recorded `EpochId` without touching the exporter (which the first
126    /// call punctured and which cannot be re-evaluated). The already
127    /// persisted operation secret tree keeps its state, so a caller retrying
128    /// an operation allocates the next generation rather than re-deriving a
129    /// consumed one.
130    ///
131    /// The emulation group must support `safe_export_secret`, which requires
132    /// the appropriate `AppDataDictionary` capability and extension wiring at
133    /// group creation. Otherwise this returns
134    /// [`SafeExportSecretError::Unsupported`] via
135    /// [`RegisterVcEmulationEpochError::SafeExportSecret`].
136    #[cfg(feature = "virtual-clients-draft")]
137    pub fn register_vc_emulation_epoch<Crypto: OpenMlsCrypto, Storage: StorageProvider>(
138        &mut self,
139        crypto: &Crypto,
140        storage: &Storage,
141    ) -> Result<EpochId, RegisterVcEmulationEpochError<Storage::Error>> {
142        // A registration consumes the forward-secure exporter, so it can run
143        // at most once per group epoch. Return the recorded epoch id if *this*
144        // epoch is already registered.
145        let registered: Option<RegisteredVcEmulationEpoch> = storage
146            .registered_vc_emulation_epoch(self.group_id())
147            .map_err(|e| {
148                log::error!(
149                    "vc: load registered emulation epoch in register_vc_emulation_epoch \
150                     failed: {e:?}"
151                );
152                RegisterVcEmulationEpochError::Storage(e)
153            })?;
154        if let Some(registered) = registered {
155            if registered.group_epoch == self.epoch() {
156                return Ok(registered.epoch_id);
157            }
158        }
159
160        let ciphersuite = self.ciphersuite();
161        let leaf_index = self.own_leaf_index();
162        let emulation_group_size = self.public_group().tree_size();
163        let bytes = self.safe_export_secret(crypto, storage, VC_COMPONENT_ID)?;
164        let emulator_epoch_secret = EmulatorEpochSecret::new(&bytes);
165        let epoch_id = emulator_epoch_secret.derive_epoch_id(crypto, ciphersuite)?;
166        let epoch_encryption_key =
167            emulator_epoch_secret.derive_epoch_encryption_key(crypto, ciphersuite)?;
168        let epoch_base_secret =
169            emulator_epoch_secret.derive_epoch_base_secret(crypto, ciphersuite)?;
170        let reuse_guard_secret =
171            emulator_epoch_secret.derive_reuse_guard_secret(crypto, ciphersuite)?;
172        let generation_id_secret =
173            emulator_epoch_secret.derive_generation_id_secret(crypto, ciphersuite)?;
174        let operation_tree = OperationSecretTree::new(epoch_base_secret, emulation_group_size);
175        let state = EmulationEpochState::new(
176            leaf_index,
177            epoch_encryption_key,
178            reuse_guard_secret,
179            generation_id_secret,
180            emulation_group_size,
181            ciphersuite,
182        );
183        let registered = RegisteredVcEmulationEpoch {
184            group_epoch: self.epoch(),
185            epoch_id,
186        };
187
188        storage
189            .write_vc_operation_tree(&registered.epoch_id, &operation_tree)
190            .map_err(|e| {
191                log::error!(
192                    "vc: persist operation tree in register_vc_emulation_epoch failed: {e:?}"
193                );
194                RegisterVcEmulationEpochError::Storage(e)
195            })?;
196        storage
197            .write_vc_emulation_epoch_state(&registered.epoch_id, &state)
198            .map_err(|e| {
199                log::error!(
200                    "vc: persist emulation epoch state in register_vc_emulation_epoch failed: {e:?}"
201                );
202                RegisterVcEmulationEpochError::Storage(e)
203            })?;
204        storage
205            .write_registered_vc_emulation_epoch(self.group_id(), &registered)
206            .map_err(|e| {
207                log::error!(
208                    "vc: record registered emulation epoch in register_vc_emulation_epoch \
209                     failed: {e:?}"
210                );
211                RegisterVcEmulationEpochError::Storage(e)
212            })?;
213
214        Ok(registered.epoch_id)
215    }
216
217    /// Returns the epoch authenticator of the current epoch.
218    pub fn epoch_authenticator(&self) -> &EpochAuthenticator {
219        self.group_epoch_secrets().epoch_authenticator()
220    }
221
222    /// Returns the resumption PSK secret of the current epoch.
223    pub fn resumption_psk_secret(&self) -> &ResumptionPskSecret {
224        self.group_epoch_secrets().resumption_psk()
225    }
226
227    /// Returns a resumption psk for a given epoch. If no resumption psk
228    /// is available for that epoch,  `None` is returned.
229    pub fn get_past_resumption_psk(&self, epoch: GroupEpoch) -> Option<&ResumptionPskSecret> {
230        self.resumption_psk_store.get(epoch)
231    }
232
233    /// Export a group info object for this group.
234    pub fn export_group_info<CryptoProvider: OpenMlsCrypto>(
235        &self,
236        crypto: &CryptoProvider,
237        signer: &impl Signer,
238        with_ratchet_tree: bool,
239    ) -> Result<MlsMessageOut, ExportGroupInfoError> {
240        self.export_group_info_with_additional_extensions(crypto, signer, with_ratchet_tree, None)
241    }
242
243    /// Export a group info object for this group, with additional extensions.
244    ///
245    ///  Returns an error if a  [`RatchetTreeExtension`] or [`ExternalPubExtension`] is added
246    ///  directly here.
247    pub fn export_group_info_with_additional_extensions<CryptoProvider: OpenMlsCrypto>(
248        &self,
249        crypto: &CryptoProvider,
250        signer: &impl Signer,
251        with_ratchet_tree: bool,
252        additional_extensions: impl IntoIterator<Item = Extension>,
253    ) -> Result<MlsMessageOut, ExportGroupInfoError> {
254        let extensions = {
255            let ratchet_tree_extension = || {
256                Extension::RatchetTree(RatchetTreeExtension::new(
257                    self.public_group().export_ratchet_tree(),
258                ))
259            };
260
261            let external_pub_extension = || -> Result<Extension, ExportGroupInfoError> {
262                let external_pub = self
263                    .group_epoch_secrets()
264                    .external_secret()
265                    .derive_external_keypair(crypto, self.ciphersuite())
266                    .map_err(LibraryError::unexpected_crypto_error)?
267                    .public;
268                Ok(Extension::ExternalPub(ExternalPubExtension::new(
269                    HpkePublicKey::from(external_pub),
270                )))
271            };
272
273            let mut extensions = if with_ratchet_tree {
274                vec![ratchet_tree_extension(), external_pub_extension()?]
275            } else {
276                vec![external_pub_extension()?]
277            };
278
279            extensions.extend(
280                additional_extensions
281                    .into_iter()
282                    .map(|extension| {
283                        if extension.as_ratchet_tree_extension().is_ok()
284                            || extension.as_external_pub_extension().is_ok()
285                        {
286                            Err(InvalidExtensionError::CannotAddDirectlyToGroupInfo)
287                        } else {
288                            Ok(extension)
289                        }
290                    })
291                    .collect::<Result<Vec<_>, _>>()?,
292            );
293
294            Extensions::from_vec(extensions)?
295        };
296
297        // Create to-be-signed group info.
298        let group_info_tbs = GroupInfoTBS::new(
299            self.context().clone(),
300            extensions,
301            self.message_secrets()
302                .confirmation_key()
303                .tag(
304                    crypto,
305                    self.ciphersuite(),
306                    self.context().confirmed_transcript_hash(),
307                )
308                .map_err(LibraryError::unexpected_crypto_error)?,
309            self.own_leaf_index(),
310        )?;
311
312        // Sign to-be-signed group info.
313        let group_info = group_info_tbs
314            .sign(signer)
315            .map_err(|_| LibraryError::custom("Signing failed"))?;
316        Ok(group_info.into())
317    }
318}