Skip to main content

openmls/key_packages/
vc.rs

1//! Key package facilities for virtual clients
2
3use openmls_traits::{
4    crypto::OpenMlsCrypto, signatures::Signer, storage::StorageProvider, types::Ciphersuite,
5    OpenMlsProvider,
6};
7use tls_codec::Serialize;
8
9use crate::{
10    binary_tree::LeafNodeIndex,
11    components::{
12        vc_derivation_info::{
13            load_vc_epoch_state_and_tree, merge_vc_derivation_info,
14            require_newest_vc_derivation_epoch, resolve_vc_leaf_dictionary, DerivationInfo,
15            DerivationInfoTbe, EpochEncryptionKey, EpochId, KeyPackageInfo, OperationSecret,
16            RetainedKeyPackageMaterial, VirtualClientOperationType, VirtualClientsError,
17        },
18        vc_operation_tree::OperationSecretTree,
19    },
20    credentials::CredentialWithKey,
21    extensions::AppDataDictionary,
22    group::GroupId,
23    key_packages::{
24        errors::KeyPackageNewError, KeyPackage, KeyPackageBuilder, KeyPackageBundle,
25        KeyPackageLeafNodeParams,
26    },
27    treesync::node::leaf_node::resolve_capabilities,
28};
29
30/// A batch of virtual-client KeyPackages a sibling can reproduce.
31///
32/// Build from a single epoch id and generation.
33#[derive(Debug)]
34pub struct VcKeyPackageBatch {
35    /// The derivation epoch the batch was built from. Hand it to
36    /// [`assemble_vc_key_package_upload`] together with the batch's
37    /// `generation`, so the upload names the epoch this batch actually consumed
38    /// rather than whichever one is newest by then.
39    ///
40    /// [`assemble_vc_key_package_upload`]: crate::components::vc_derivation_info::assemble_vc_key_package_upload
41    pub epoch_id: EpochId,
42    /// The `key_package` operation generation consumed for the whole batch.
43    pub generation: u32,
44    /// One entry per KeyPackage built, in batch-index order. Never empty.
45    pub key_packages: Vec<(
46        KeyPackageBundle,
47        crate::components::vc_derivation_info::KeyPackageInfo,
48    )>,
49}
50
51/// A builder for a batch of virtual-client KeyPackages a sibling can reproduce.
52///
53/// Allows to build heterogeneous batch of key packages, e.g. non-last-resort and last-restort, or
54/// packages with different ciphersuits. Return the batch on [`Self::finalize`]. Dropping the
55/// builder without calling `finalize` burns no generation.
56#[derive(Debug)]
57pub struct VcKeyPackageBatchBuilder {
58    epoch_id: EpochId,
59    /// Ciphersuite of the emulation group
60    emulation_ciphersuite: Ciphersuite,
61    epoch_encryption_key: EpochEncryptionKey,
62    emulation_leaf_index: LeafNodeIndex,
63    generation: u32,
64    operation_secret: OperationSecret,
65    /// Advanced in memory by new()
66    ///
67    /// Only persisted in finalize(), so dropped builder burns not generation.
68    operation_tree: OperationSecretTree,
69    key_packages: Vec<(KeyPackageBundle, KeyPackageInfo)>,
70}
71
72impl VcKeyPackageBatchBuilder {
73    /// Load the newest derivation epoch of the emulation group named by
74    /// `emulation_group_id` and allocate the next generation of its key package
75    /// operation ratchet.
76    ///
77    /// The batch uses the newest derivation epoch of the emulation group, which
78    /// is what the draft requires of every new virtual-client operation. The
79    /// epoch is resolved from the emulation group's current state.
80    ///
81    /// Nothing is persisted yet. Dropping the builder without calling `finalize` burns no
82    /// generation.
83    #[openmls_traits::maybe_async]
84    pub async fn new(
85        provider: &impl OpenMlsProvider,
86        emulation_group_id: &GroupId,
87    ) -> Result<Self, KeyPackageNewError> {
88        Self::with_capacity(provider, emulation_group_id, 0).await
89    }
90
91    /// Same as [`Self::new`], but with a capacity hint for the number of key packages.
92    #[openmls_traits::maybe_async]
93    pub async fn with_capacity(
94        provider: &impl OpenMlsProvider,
95        emulation_group_id: &GroupId,
96        capacity: usize,
97    ) -> Result<Self, KeyPackageNewError> {
98        let epoch_id =
99            require_newest_vc_derivation_epoch(provider.storage(), emulation_group_id).await?;
100        Self::with_capacity_at_epoch(provider, epoch_id, capacity).await
101    }
102
103    /// Same as [`Self::with_capacity`], but for an explicitly named derivation
104    /// epoch instead of the emulation group's newest one.
105    #[openmls_traits::maybe_async]
106    pub(crate) async fn with_capacity_at_epoch(
107        provider: &impl OpenMlsProvider,
108        epoch_id: EpochId,
109        capacity: usize,
110    ) -> Result<Self, KeyPackageNewError> {
111        let (state, mut operation_tree) = load_vc_epoch_state_and_tree(provider, &epoch_id).await?;
112        let (emulation_leaf_index, epoch_encryption_key, emulation_ciphersuite) =
113            state.into_parts();
114        let (generation, operation_secret) = operation_tree.next_operation_secret(
115            provider.crypto(),
116            emulation_ciphersuite,
117            &epoch_id,
118            emulation_leaf_index,
119            VirtualClientOperationType::KeyPackage,
120            b"",
121        )?;
122        Ok(Self {
123            epoch_id,
124            emulation_ciphersuite,
125            epoch_encryption_key,
126            emulation_leaf_index,
127            generation,
128            operation_secret,
129            operation_tree,
130            key_packages: Vec::with_capacity(capacity),
131        })
132    }
133
134    /// Build one key package at the next index.
135    ///
136    /// The key package builder carries the per-step config: key package extensions (incl. last
137    /// resort), leaf node extensions, capabilities, lifetime.
138    ///
139    /// If building fails, the builder is left in a valid state:
140    ///
141    /// - No storage is touched
142    /// - No generation is burned
143    /// - The failed index isn't consumed
144    pub fn add_key_package(
145        &mut self,
146        builder: KeyPackageBuilder,
147        ciphersuite: Ciphersuite,
148        crypto: &impl OpenMlsCrypto,
149        signer: &impl Signer,
150        credential_with_key: CredentialWithKey,
151    ) -> Result<&KeyPackageInfo, KeyPackageNewError> {
152        if ciphersuite.signature_algorithm() != signer.signature_scheme() {
153            return Err(KeyPackageNewError::CiphersuiteSignatureSchemeMismatch);
154        }
155
156        crypto
157            .supports(ciphersuite)
158            .map_err(|_| KeyPackageNewError::UnsupportedCiphersuite(ciphersuite))?;
159
160        // Resolve and validate the leaf configuration
161        let resolved_dictionary = resolve_vc_leaf_dictionary(
162            builder.leaf_node_capabilities.as_ref(),
163            builder.leaf_node_extensions.as_ref(),
164            None,
165        )?;
166
167        let key_package_index = self.key_packages.len() as u32;
168        self.key_packages.push(self.build_vc_key_package_for_index(
169            builder,
170            ciphersuite,
171            crypto,
172            signer,
173            credential_with_key,
174            &resolved_dictionary,
175            key_package_index,
176        )?);
177
178        let (_, info) = self.key_packages.last().expect("logic error: just pushed");
179        Ok(info)
180    }
181
182    /// Finalize the batch.
183    ///
184    /// Persists the operation tree, a [`RetainedKeyPackageMaterial`] per key
185    /// package and the key packages. The material is what a sibling stores when
186    /// it processes the batch's [`KeyPackageUpload`]. It keeps the batch's
187    /// derivation epoch alive for as long as a key package can still be used to
188    /// join, also after the epoch was superseded. It goes with the key package,
189    /// see [`StorageProvider::delete_key_package`].
190    ///
191    /// The operation is not atomic. On failure, the generation should be
192    /// considered as burned. Few orphaned key packages may be left in storage.
193    ///
194    /// [`KeyPackageUpload`]:
195    ///     crate::components::vc_derivation_info::KeyPackageUpload
196    #[openmls_traits::maybe_async]
197    pub async fn finalize(
198        self,
199        provider: &impl OpenMlsProvider,
200    ) -> Result<VcKeyPackageBatch, KeyPackageNewError> {
201        if self.key_packages.is_empty() {
202            return Err(KeyPackageNewError::EmptyBatch);
203        }
204
205        let mut materials = Vec::with_capacity(self.key_packages.len());
206        for (_, info) in &self.key_packages {
207            let key_package_seed_secret = self.operation_secret.derive_key_package_seed_secret(
208                provider.crypto(),
209                info.cipher_suite,
210                info.key_package_index,
211            )?;
212            let material = RetainedKeyPackageMaterial {
213                epoch_id: self.epoch_id.clone(),
214                leaf_index: self.emulation_leaf_index,
215                generation: self.generation,
216                key_package_ciphersuite: info.cipher_suite,
217                key_package_index: info.key_package_index,
218                key_package_seed_secret,
219                key_package_extensions: info.extensions.clone(),
220            };
221            materials.push((info.key_package_ref.clone(), material));
222        }
223
224        // Persist the advanced operation tree before the KeyPackages it backs.
225        // If a KeyPackage write fails after this, the burned generation is
226        // harmless, but writing KeyPackages first would let the next batch
227        // reuse the same key material under an unconsumed generation.
228        provider
229            .storage()
230            .write_retained_key_package_material_batch(
231                &self.epoch_id,
232                &self.operation_tree,
233                &materials,
234            )
235            .await
236            .map_err(|e| {
237                log::error!(
238                    "vc: persist batch key package material in build_vc_batch failed: {e:?}"
239                );
240                VirtualClientsError::StorageError
241            })?;
242        for (full_kp, info) in &self.key_packages {
243            provider
244                .storage()
245                .write_key_package(&info.key_package_ref, full_kp)
246                .await
247                .map_err(|_| KeyPackageNewError::StorageError)?;
248        }
249
250        Ok(VcKeyPackageBatch {
251            epoch_id: self.epoch_id,
252            generation: self.generation,
253            key_packages: self.key_packages,
254        })
255    }
256
257    /// Derive and build a single KeyPackage at `key_package_index`
258    #[expect(clippy::too_many_arguments)]
259    fn build_vc_key_package_for_index(
260        &self,
261        mut builder: KeyPackageBuilder,
262        ciphersuite: Ciphersuite,
263        crypto: &impl OpenMlsCrypto,
264        signer: &impl Signer,
265        credential_with_key: CredentialWithKey,
266        resolved_dictionary: &AppDataDictionary,
267        key_package_index: u32,
268    ) -> Result<(KeyPackageBundle, KeyPackageInfo), KeyPackageNewError> {
269        let seed = self.operation_secret.derive_key_package_seed_secret(
270            crypto,
271            ciphersuite,
272            key_package_index,
273        )?;
274        let init_key_pair = seed
275            .derive_init_key_secret(crypto, ciphersuite)?
276            .generate_init_key_pair(crypto, ciphersuite)?;
277        let encryption_key_pair = seed
278            .derive_encryption_key_secret(crypto, ciphersuite)?
279            .generate_encryption_key_pair(crypto, ciphersuite)?;
280
281        // Wrap the TBE bound to the new leaf via its serialized encryption key.
282        // The leaf dictionary was resolved and validated once for the whole
283        // batch, so reuse a clone here.
284        let leaf_encryption_key = encryption_key_pair
285            .public_key()
286            .tls_serialize_detached()
287            .map_err(VirtualClientsError::from)?;
288        let tbe = DerivationInfoTbe::KeyPackage {
289            leaf_index: self.emulation_leaf_index,
290            generation: self.generation,
291            key_package_index,
292        };
293        let derivation_info = DerivationInfo::encrypt(
294            crypto,
295            self.emulation_ciphersuite,
296            &self.epoch_encryption_key,
297            self.epoch_id.clone(),
298            &leaf_encryption_key,
299            &tbe,
300        )?;
301        let derivation_info_bytes = derivation_info
302            .tls_serialize_detached()
303            .map_err(VirtualClientsError::from)?;
304        builder.ensure_last_resort()?;
305        let leaf_node_extensions = merge_vc_derivation_info(
306            builder.leaf_node_extensions.as_ref(),
307            resolved_dictionary.clone(),
308            derivation_info_bytes,
309        )
310        .map_err(KeyPackageNewError::LibraryError)?;
311
312        let (capabilities, capabilities_policy) = resolve_capabilities(
313            builder.leaf_node_capabilities.clone(),
314            builder.capabilities_policy,
315        );
316        let leaf_node_params = KeyPackageLeafNodeParams {
317            lifetime: builder.key_package_lifetime.unwrap_or_default(),
318            capabilities,
319            extensions: leaf_node_extensions,
320            capabilities_policy,
321        };
322        let (key_package, encryption_key_pair) = KeyPackage::new_from_vc_keys(
323            ciphersuite,
324            signer,
325            credential_with_key,
326            builder.key_package_extensions.unwrap_or_default(),
327            leaf_node_params,
328            init_key_pair.public.into(),
329            encryption_key_pair,
330        )?;
331
332        let key_package_ref = key_package.hash_ref(crypto)?;
333        let extensions = key_package.extensions().clone();
334        let full_kp = KeyPackageBundle {
335            key_package,
336            private_init_key: init_key_pair.private,
337            private_encryption_key: encryption_key_pair.private_key().clone(),
338        };
339
340        Ok((
341            full_kp,
342            KeyPackageInfo {
343                key_package_ref,
344                cipher_suite: ciphersuite,
345                key_package_index,
346                extensions,
347            },
348        ))
349    }
350}