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