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