Skip to main content

openmls/components/
vc_derivation_info.rs

1use openmls_traits::{
2    crypto::OpenMlsCrypto,
3    types::{Ciphersuite, CryptoError},
4    OpenMlsProvider,
5};
6use serde::{Deserialize, Serialize};
7use thiserror::Error;
8use tls_codec::{
9    DeserializeBytes, SecretVLByteVec, Serialize as _, Size as _, TlsDeserializeBytes,
10    TlsSerialize, TlsSize, VLByteSlice, VLByteVec,
11};
12
13use crate::{
14    binary_tree::{array_representation::TreeSize, LeafNodeIndex},
15    ciphersuite::{hash_ref::KeyPackageRef, Secret},
16    group::{GroupEpoch, GroupId},
17    key_packages::InitKey,
18    messages::PathSecret,
19    treesync::node::encryption_keys::EncryptionKeyPair,
20};
21
22/// Component ID under which the virtual-clients derivation info is carried in
23/// the leaf node's `app_data_dictionary` extension.
24///
25/// `0x667A` is the temporary, random value until the draft is further along the
26/// publication process.
27pub const VC_COMPONENT_ID: u16 = 0x667A;
28
29// Operation-secret child labels. Each child is derived from the per-operation
30// secret produced by the per-epoch operation secret tree. `Encryption Key`
31// and `Path Generation` cover the `leaf_node` commit path, and `Init Key`
32// covers the `key_package` operation path. The spec also defines a
33// `Signature Key` child, which together with the operation paths that consume
34// it is deferred to a follow-up PR.
35const ENCRYPTION_KEY_LABEL: &str = "Encryption Key";
36const PATH_GENERATION_LABEL: &str = "Path Generation";
37const INIT_KEY_LABEL: &str = "Init Key";
38/// `ImportSecret` label for the per-KeyPackage seed secret derived from a
39/// `key_package` operation secret (mls-virtual-clients draft, batch KeyPackage
40/// derivation). One operation secret covers a batch of KeyPackages, and each
41/// KeyPackage's seed is imported from it using the KeyPackage's ciphersuite
42/// and index as the context.
43const KEY_PACKAGE_SEED_LABEL: &str = "vc key package seed";
44/// `ImportSecret` label for `target_operation_secret` of a `leaf_node`:
45/// imports operation secret into the higher-level group's ciphersuite.
46const TARGET_OPERATION_LABEL: &str = "vc target operation";
47/// `DeriveSecret` label for the epoch-0 `epoch_secret` a group creator derives
48/// from its KeyPackage seed secret (mls-virtual-clients draft, group creation).
49const GROUP_CREATION_LABEL: &str = "Group Creation";
50
51/// `ExpandWithLabel` label for the [`DerivationInfoTbe`] AEAD key derived
52/// from the per-epoch [`EpochEncryptionKey`].
53const DERIVATION_INFO_KEY_LABEL: &str = "key";
54/// `ExpandWithLabel` label for the [`DerivationInfoTbe`] AEAD nonce derived
55/// from the per-epoch [`EpochEncryptionKey`].
56const DERIVATION_INFO_NONCE_LABEL: &str = "nonce";
57
58const EPOCH_ID_LABEL: &str = "Epoch ID";
59const EPOCH_ENCRYPTION_KEY_LABEL: &str = "Encryption Key";
60const EPOCH_BASE_SECRET_LABEL: &str = "Base Secret";
61/// `DeriveSecret` label for [`ReuseGuardSecret`].
62const REUSE_GUARD_LABEL: &str = "Reuse Guard";
63/// `DeriveSecret` label for [`GenerationIdSecret`].
64const GENERATION_ID_LABEL: &str = "Generation ID Secret";
65/// `ExpandWithLabel` label for a [`GenerationId`] derived from a
66/// [`GenerationIdSecret`] over a serialized [`PrivateMessageContext`]
67/// (mls-virtual-clients draft, generation-ID section).
68const GENERATION_ID_EXPAND_LABEL: &str = "generation id";
69/// `ExpandWithLabel` label for the 16-byte FF1 PRP key derived from a
70/// [`ReuseGuardSecret`] (mls-virtual-clients draft, Reuse Guard section).
71const REUSE_GUARD_PRP_KEY_LABEL: &str = "reuse guard";
72/// FF1 PRP key length in bytes (AES-128).
73const PRP_KEY_LEN: usize = 16;
74
75/// Errors that can occur while processing virtual-clients derivation info.
76#[derive(Error, Debug, PartialEq, Clone)]
77pub enum VirtualClientsError {
78    /// The derivation-info bytes failed to deserialize.
79    #[error("Failed to deserialize derivation info.")]
80    DerivationInfoMalformed,
81    /// AEAD decryption of the encrypted derivation info failed (wrong key,
82    /// tampered ciphertext, or mismatched AAD).
83    #[error("Failed to decrypt derivation info.")]
84    DerivationInfoDecryptionFailed,
85    /// No virtual-clients operation secret tree was registered for this
86    /// epoch.
87    #[error("No virtual-clients operation secret tree for this epoch.")]
88    MissingOperationTree,
89    /// No virtual-clients `EmulationEpochState` was registered for this
90    /// epoch, or it has been deleted.
91    #[error("No virtual-clients emulation-epoch state for this epoch.")]
92    MissingEmulationEpochState,
93    /// Loading or storing virtual-clients state via the storage provider
94    /// failed.
95    #[error("Virtual-clients storage error")]
96    StorageError,
97    /// The leaf encryption key in the path does not match the key derived
98    /// from the path secret.
99    #[error("Leaf encryption key from path does not match the derived key.")]
100    EncryptionKeyMismatch,
101    /// A cryptographic operation failed during virtual-clients processing.
102    #[error("Cryptographic operation failed.")]
103    CryptoError(#[from] CryptoError),
104    /// Hash function produced output of unexpected length.
105    #[error(
106        "Hash function produced output of length {actual_length}, expected {expected_length}."
107    )]
108    HashOutputLengthMismatch {
109        /// The number of bytes in the hash output.
110        actual_length: usize,
111        /// The required number of bytes in the hash output.
112        expected_length: usize,
113    },
114    /// TLS encoding/decoding of a virtual-clients structure failed. Covers
115    /// both serialization on the sender side and deserialization of the
116    /// decrypted `DerivationInfoTbe` on the receiver side.
117    #[error("TLS codec error: {0}")]
118    Tls(#[from] tls_codec::Error),
119    /// The leaf carrying (or about to carry) a VC derivation-info entry
120    /// does not declare `AppDataDictionary` in its capabilities.
121    #[error("Leaf does not declare AppDataDictionary support in its capabilities.")]
122    AppDataDictionaryNotSupported,
123    /// The leaf's `AppDataDictionary` extension is missing the
124    /// `AppComponents` entry, or that entry does not list
125    /// [`VC_COMPONENT_ID`].
126    #[error("Leaf's AppComponents entry does not list the virtual-clients component id.")]
127    VcComponentNotListed,
128    /// The requested leaf index lies outside the operation secret tree.
129    #[error("Leaf index is outside the operation secret tree.")]
130    IndexOutOfBounds,
131    /// The operation secret for the requested generation was already derived
132    /// and deleted for forward secrecy.
133    #[error("The operation secret for this generation was already consumed.")]
134    OperationGenerationConsumed,
135    /// The requested operation generation lies too far beyond the current
136    /// ratchet head (see `MAXIMUM_FORWARD_DISTANCE` in the operation secret
137    /// tree).
138    #[error("The requested operation generation is too far beyond the ratchet head.")]
139    OperationGenerationTooDistant,
140    /// An operation ratchet has reached the maximum generation.
141    #[error("Operation ratchet generation has reached `u32::MAX`.")]
142    OperationRatchetTooLong,
143    /// An unrecoverable error has occurred due to a bug in the
144    /// implementation.
145    #[error("An unrecoverable error has occurred due to a bug in the implementation.")]
146    LibraryError,
147    /// The `KeyPackageUpload` lists the same `key_package_index` more than
148    /// once. Each batch index must appear at most once.
149    #[error("KeyPackageUpload contains a duplicate key_package_index: {0}.")]
150    DuplicateKeyPackageIndex(u32),
151    /// The `KeyPackageUpload` lists the same [`KeyPackageRef`] more than once.
152    /// Each KeyPackage reference must appear at most once.
153    #[error("KeyPackageUpload contains a duplicate KeyPackageRef.")]
154    DuplicateKeyPackageRef,
155}
156
157/// Per-emulation-epoch root secret. Sourced internally by
158/// [`MlsGroup::register_vc_emulation_epoch`] from the emulation group's
159/// `safe_export_secret(VC_COMPONENT_ID)`.
160///
161/// [`MlsGroup::register_vc_emulation_epoch`]: crate::group::MlsGroup::register_vc_emulation_epoch
162#[derive(Debug, Serialize, Deserialize)]
163pub(crate) struct EmulatorEpochSecret(Secret);
164
165impl EmulatorEpochSecret {
166    /// Construct an `EmulatorEpochSecret` from raw bytes. Bytes are
167    /// expected to be the output of the emulation group's
168    /// `safe_export_secret(VC_COMPONENT_ID)`.
169    pub(crate) fn new(bytes: &[u8]) -> Self {
170        Self(Secret::from_slice(bytes))
171    }
172
173    pub(crate) fn derive_epoch_id(
174        &self,
175        crypto: &impl OpenMlsCrypto,
176        ciphersuite: Ciphersuite,
177    ) -> Result<EpochId, VirtualClientsError> {
178        let secret = self.0.derive_secret(crypto, ciphersuite, EPOCH_ID_LABEL)?;
179        Ok(EpochId(secret.as_slice().to_vec().into()))
180    }
181
182    /// Derive the per-epoch [`EpochEncryptionKey`]. The key is a KDF
183    /// secret (the per-leaf AEAD key and nonce are expanded from it), so
184    /// it is derived at the KDF's hash length.
185    pub(crate) fn derive_epoch_encryption_key(
186        &self,
187        crypto: &impl OpenMlsCrypto,
188        ciphersuite: Ciphersuite,
189    ) -> Result<EpochEncryptionKey, VirtualClientsError> {
190        let secret = self
191            .0
192            .derive_secret(crypto, ciphersuite, EPOCH_ENCRYPTION_KEY_LABEL)?;
193        Ok(EpochEncryptionKey(secret))
194    }
195
196    pub(crate) fn derive_epoch_base_secret(
197        &self,
198        crypto: &impl OpenMlsCrypto,
199        ciphersuite: Ciphersuite,
200    ) -> Result<Secret, VirtualClientsError> {
201        Ok(self
202            .0
203            .derive_secret(crypto, ciphersuite, EPOCH_BASE_SECRET_LABEL)?)
204    }
205
206    /// Derive the per-emulation-epoch [`ReuseGuardSecret`].
207    pub(crate) fn derive_reuse_guard_secret(
208        &self,
209        crypto: &impl OpenMlsCrypto,
210        ciphersuite: Ciphersuite,
211    ) -> Result<ReuseGuardSecret, VirtualClientsError> {
212        let secret = self
213            .0
214            .derive_secret(crypto, ciphersuite, REUSE_GUARD_LABEL)?;
215        Ok(ReuseGuardSecret(secret))
216    }
217
218    /// Derive the per-emulation-epoch [`GenerationIdSecret`].
219    pub(crate) fn derive_generation_id_secret(
220        &self,
221        crypto: &impl OpenMlsCrypto,
222        ciphersuite: Ciphersuite,
223    ) -> Result<GenerationIdSecret, VirtualClientsError> {
224        let secret = self
225            .0
226            .derive_secret(crypto, ciphersuite, GENERATION_ID_LABEL)?;
227        Ok(GenerationIdSecret(secret))
228    }
229}
230
231/// Per-emulation-epoch secret used to derive the FF1 PRP key for
232/// `reuse_guard` values sent by this virtual client. Derived from
233/// [`EmulatorEpochSecret`] via [`EmulatorEpochSecret::derive_reuse_guard_secret`].
234#[derive(Debug, Serialize, Deserialize)]
235pub(crate) struct ReuseGuardSecret(Secret);
236
237impl ReuseGuardSecret {
238    /// Test-only constructor from raw bytes.
239    #[cfg(test)]
240    pub(crate) fn from_secret_for_tests(secret: Secret) -> Self {
241        Self(secret)
242    }
243
244    /// Derive the 16-byte FF1 PRP key for a single application message:
245    ///
246    /// ```text
247    /// prp_key = ExpandWithLabel(reuse_guard_secret, "reuse guard",
248    ///                           key_schedule_nonce, 16)
249    /// ```
250    ///
251    /// `ciphersuite` is the emulation group's ciphersuite, stored on
252    /// [`EmulationEpochState`].
253    pub(crate) fn derive_prp_key(
254        &self,
255        crypto: &impl OpenMlsCrypto,
256        ciphersuite: Ciphersuite,
257        key_schedule_nonce: &[u8],
258    ) -> Result<[u8; PRP_KEY_LEN], VirtualClientsError> {
259        let key = self.0.kdf_expand_label(
260            crypto,
261            ciphersuite,
262            REUSE_GUARD_PRP_KEY_LABEL,
263            key_schedule_nonce,
264            PRP_KEY_LEN,
265        )?;
266        key.as_slice()
267            .try_into()
268            .map_err(|_| VirtualClientsError::HashOutputLengthMismatch {
269                actual_length: key.as_slice().len(),
270                expected_length: PRP_KEY_LEN,
271            })
272    }
273}
274
275/// Per-emulation-epoch secret used to derive generation IDs for DS
276/// collision detection (mls-virtual-clients draft, "Coordinating ratchet
277/// generations with the DS" section). Derived from [`EmulatorEpochSecret`]
278/// via [`EmulatorEpochSecret::derive_generation_id_secret`].
279#[derive(Debug, Serialize, Deserialize)]
280pub(crate) struct GenerationIdSecret(Secret);
281
282impl GenerationIdSecret {
283    /// Derive the [`GenerationId`] for a message sent with the given
284    /// [`PrivateMessageContext`]:
285    ///
286    /// ```text
287    /// generation_id = ExpandWithLabel(generation_id_secret, "generation id",
288    ///                                 PrivateMessageContext, Kdf.Nh)
289    /// ```
290    ///
291    /// `ciphersuite` is the emulation group's ciphersuite, the same one the
292    /// `generation_id_secret` was derived under.
293    fn derive_generation_id(
294        &self,
295        crypto: &impl OpenMlsCrypto,
296        ciphersuite: Ciphersuite,
297        context: &PrivateMessageContext<'_>,
298    ) -> Result<GenerationId, VirtualClientsError> {
299        let context_bytes = context.tls_serialize_detached()?;
300        let generation_id = self.0.kdf_expand_label(
301            crypto,
302            ciphersuite,
303            GENERATION_ID_EXPAND_LABEL,
304            &context_bytes,
305            ciphersuite.hash_length(),
306        )?;
307        Ok(GenerationId(generation_id.as_slice().to_vec().into()))
308    }
309}
310
311/// Which ratchet a `PrivateMessageContext` refers to
312/// (mls-virtual-clients draft `RatchetType`):
313///
314/// ```text
315/// enum {
316///   reserved(0),
317///   application(1),
318///   handshake(2),
319///   (255)
320/// } RatchetType
321/// ```
322///
323/// [`Application`](Self::Application) covers application messages, and
324/// [`Handshake`](Self::Handshake) covers proposals and commits framed as
325/// PrivateMessages in a higher-level group. Both draw a generation ID from
326/// their respective per-leaf ratchet.
327#[derive(Debug, Clone, Copy, PartialEq, Eq, TlsSize, TlsSerialize)]
328#[repr(u8)]
329pub enum RatchetType {
330    /// The per-leaf application-message ratchet.
331    Application = 1,
332    /// The per-leaf handshake-message ratchet.
333    Handshake = 2,
334}
335
336/// Context a [`GenerationId`] is derived over (mls-virtual-clients draft):
337///
338/// ```text
339/// struct {
340///   opaque group_id<V>;
341///   uint64 epoch;
342///   uint32 generation;
343///   RatchetType ratchet_type;
344/// } PrivateMessageContext
345/// ```
346///
347/// `group_id` and `epoch` identify the higher-level group and its epoch at
348/// the time the message is sent, `generation` is the ratchet generation used
349/// for encryption, and `ratchet_type` distinguishes the application and
350/// handshake ratchets. Only ever serialized as a derivation context, never
351/// parsed back, so it borrows its `group_id` and needs serialization only.
352#[derive(Debug, TlsSize, TlsSerialize)]
353pub(crate) struct PrivateMessageContext<'a> {
354    group_id: VLByteSlice<'a>,
355    epoch: u64,
356    generation: u32,
357    ratchet_type: RatchetType,
358}
359
360/// A per-message generation ID a virtual client attaches to a fanned-out
361/// PrivateMessage so a strongly-consistent DS can detect generation
362/// collisions between siblings, per higher-level group, per higher-level
363/// group epoch, and per ratchet type (mls-virtual-clients draft).
364///
365/// Derived from the emulation epoch's `GenerationIdSecret` over a
366/// `PrivateMessageContext`. The value is opaque to the application: it is
367/// produced by [`MlsGroup::create_unconfirmed_message`] and handed to the DS,
368/// which compares it for equality across siblings.
369///
370/// [`MlsGroup::create_unconfirmed_message`]: crate::group::MlsGroup::create_unconfirmed_message
371#[derive(Debug, Clone, PartialEq, Eq, TlsSize, TlsSerialize, TlsDeserializeBytes)]
372pub struct GenerationId(VLByteVec);
373
374impl GenerationId {
375    /// The raw generation-ID bytes the application hands to the DS.
376    pub fn as_slice(&self) -> &[u8] {
377        self.0.as_slice()
378    }
379}
380
381/// The virtual-clients derivation info carried in the leaf node's
382/// `app_data_dictionary` extension under [`VC_COMPONENT_ID`]
383/// (mls-virtual-clients draft):
384///
385/// ```text
386/// struct {
387///   opaque epoch_id<V>;
388///   opaque ciphertext<V>;
389/// } DerivationInfo
390/// ```
391///
392/// `ciphertext` is the AEAD-wrapped [`DerivationInfoTbe`], encrypted in the
393/// emulation group's ciphersuite with key and nonce derived from the
394/// per-epoch [`EpochEncryptionKey`] and the carrying leaf's serialized
395/// `encryption_key`, with `epoch_id` as AAD.
396#[derive(Debug, TlsSize, TlsSerialize, TlsDeserializeBytes)]
397pub(crate) struct DerivationInfo {
398    epoch_id: EpochId,
399    ciphertext: VLByteVec,
400}
401
402impl DerivationInfo {
403    /// Encrypt `tbe` under the per-epoch AEAD key, binding it to the leaf
404    /// that carries the resulting derivation info via the leaf's serialized
405    /// `encryption_key` (the key/nonce derivation context) and to
406    /// `epoch_id` (the AAD).
407    pub(crate) fn encrypt(
408        crypto: &impl OpenMlsCrypto,
409        ciphersuite: Ciphersuite,
410        key: &EpochEncryptionKey,
411        epoch_id: EpochId,
412        leaf_encryption_key: &[u8],
413        tbe: &DerivationInfoTbe,
414    ) -> Result<Self, VirtualClientsError> {
415        let (aead_key, aead_nonce) =
416            key.derive_key_nonce(crypto, ciphersuite, leaf_encryption_key)?;
417        let payload = tbe.tls_serialize_detached()?;
418        let ciphertext = crypto.aead_encrypt(
419            ciphersuite.aead_algorithm(),
420            aead_key.as_slice(),
421            payload.as_slice(),
422            aead_nonce.as_slice(),
423            epoch_id.0.as_slice(),
424        )?;
425        Ok(Self {
426            epoch_id,
427            ciphertext: ciphertext.into(),
428        })
429    }
430
431    pub(crate) fn epoch_id(&self) -> &EpochId {
432        &self.epoch_id
433    }
434
435    /// Decrypt the wrapped [`DerivationInfoTbe`]. `leaf_encryption_key` is
436    /// the serialized `encryption_key` of the leaf node that carries this
437    /// derivation info.
438    pub(crate) fn decrypt(
439        &self,
440        crypto: &impl OpenMlsCrypto,
441        ciphersuite: Ciphersuite,
442        key: &EpochEncryptionKey,
443        leaf_encryption_key: &[u8],
444        operation_type: VirtualClientOperationType,
445    ) -> Result<DerivationInfoTbe, VirtualClientsError> {
446        let (aead_key, aead_nonce) =
447            key.derive_key_nonce(crypto, ciphersuite, leaf_encryption_key)?;
448        let plaintext = crypto
449            .aead_decrypt(
450                ciphersuite.aead_algorithm(),
451                aead_key.as_slice(),
452                self.ciphertext.as_slice(),
453                aead_nonce.as_slice(),
454                self.epoch_id.0.as_slice(),
455            )
456            .map_err(|e| {
457                log::error!("vc: aead decrypt derivation info failed: {e:?}");
458                VirtualClientsError::DerivationInfoDecryptionFailed
459            })?;
460        DerivationInfoTbe::deserialize_for_operation(&plaintext, operation_type)
461    }
462}
463
464/// Identifier of an emulation epoch's registered virtual-clients state.
465/// Derived deterministically from the emulation group's
466/// `safe_export_secret(VC_COMPONENT_ID)` by
467/// [`MlsGroup::register_vc_emulation_epoch`].
468///
469/// [`MlsGroup::register_vc_emulation_epoch`]: crate::group::MlsGroup::register_vc_emulation_epoch
470#[derive(
471    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TlsSize, TlsSerialize, TlsDeserializeBytes,
472)]
473pub struct EpochId(VLByteVec);
474
475impl EpochId {
476    /// Create an epoch ID from raw bytes.
477    pub fn new(bytes: Vec<u8>) -> Self {
478        Self(bytes.into())
479    }
480
481    /// The raw epoch-ID bytes.
482    pub fn as_bytes(&self) -> &[u8] {
483        self.0.as_slice()
484    }
485}
486
487/// Wire struct a virtual client hands to a sibling so the sibling can fetch
488/// and process the matching KeyPackage (mls-virtual-clients draft):
489///
490/// ```text
491/// struct {
492///   opaque key_package_ref<V>;
493///   CipherSuite cipher_suite;
494///   uint32 key_package_index;
495/// } KeyPackageInfo
496/// ```
497///
498/// `key_package_ref` is the [`KeyPackageRef`] (a [`HashReference`]) of the
499/// KeyPackage built by [`KeyPackageBuilder::build_vc_batch`]. `key_package_index`
500/// is the KeyPackage's position within the `key_package` operation batch: one
501/// operation secret covers the whole batch and each KeyPackage's seed is
502/// derived from it under this index.
503///
504/// [`HashReference`]: crate::ciphersuite::hash_ref::HashReference
505/// [`KeyPackageBuilder::build_vc_batch`]: crate::key_packages::KeyPackageBuilder::build_vc_batch
506#[derive(Debug, TlsSize, TlsSerialize, TlsDeserializeBytes)]
507pub struct KeyPackageInfo {
508    /// Hash reference of the virtual client's KeyPackage.
509    pub key_package_ref: KeyPackageRef,
510    /// Ciphersuite of the virtual client's KeyPackage.
511    pub cipher_suite: Ciphersuite,
512    /// Position of this KeyPackage within the operation batch.
513    pub key_package_index: u32,
514}
515
516/// Wire struct a virtual client uploads to a sibling so the sibling learns
517/// about the KeyPackages the virtual client published for an emulation epoch
518/// (mls-virtual-clients draft):
519///
520/// ```text
521/// struct {
522///   opaque epoch_id<V>;
523///   uint32 leaf_index;
524///   uint32 generation;
525///   KeyPackageInfo key_package_info<V>;
526/// } KeyPackageUpload
527/// ```
528///
529/// `epoch_id` identifies the emulation epoch the KeyPackages belong to.
530/// `leaf_index` is the uploading client's emulation-group leaf index at that
531/// epoch. The receiver stores this leaf index: the KeyPackage operation
532/// secret was allocated from the uploader's per-leaf ratchet, so a sibling
533/// rederiving the KeyPackage material must walk that same leaf's ratchet, not
534/// its own. `generation` is the single `key_package` operation generation
535/// consumed for the whole batch. `key_package_info` carries one
536/// [`KeyPackageInfo`] per uploaded KeyPackage, each with its index within the
537/// batch.
538#[derive(Debug, TlsSize, TlsSerialize, TlsDeserializeBytes)]
539pub struct KeyPackageUpload {
540    /// Emulation epoch the uploaded KeyPackages belong to.
541    pub epoch_id: EpochId,
542    /// Uploading client's emulation-group leaf index at that epoch.
543    pub leaf_index: LeafNodeIndex,
544    /// Operation-ratchet generation consumed for the whole batch.
545    pub generation: u32,
546    /// One entry per uploaded KeyPackage.
547    pub key_package_info: Vec<KeyPackageInfo>,
548}
549
550/// Per-`KeyPackageRef` material a sibling retains when it processes a
551/// [`KeyPackageUpload`]. It captures what the Welcome path needs to later
552/// rederive the KeyPackage's init and leaf-encryption keys without touching
553/// the operation tree: the per-KeyPackage seed secret, plus the emulation
554/// epoch, leaf index, generation, and batch index used to validate the leaf
555/// found in the ratchet tree.
556///
557/// The seed is pinned here at upload-processing time so the Welcome path stays
558/// independent of the operation tree's bounded out-of-order tolerance: a batch
559/// can hold more KeyPackages than that tolerance, and Welcomes can arrive in
560/// any order, yet every seed remains available because the single batch
561/// generation is consumed once and each seed is stored alongside its index.
562#[derive(Debug, Serialize, Deserialize)]
563pub struct RetainedKeyPackageMaterial {
564    /// Emulation epoch the KeyPackage belongs to.
565    pub epoch_id: EpochId,
566    /// Uploader's emulation-group leaf index, identifying the operation
567    /// ratchet the batch generation was allocated from.
568    pub leaf_index: LeafNodeIndex,
569    /// Operation-ratchet generation consumed for the whole batch.
570    pub generation: u32,
571    /// Ciphersuite of the KeyPackage.
572    pub key_package_ciphersuite: Ciphersuite,
573    /// Position of this KeyPackage within the batch.
574    pub key_package_index: u32,
575    /// Per-KeyPackage seed secret from which the init and leaf-encryption keys
576    /// are derived at Welcome time.
577    pub key_package_seed_secret: KeyPackageSeedSecret,
578}
579
580/// Reject a batch whose [`KeyPackageInfo`] entries are not all distinct.
581///
582/// Returns [`VirtualClientsError::DuplicateKeyPackageIndex`] if any
583/// `key_package_index` repeats, and
584/// [`VirtualClientsError::DuplicateKeyPackageRef`] if any `key_package_ref`
585/// repeats. A duplicate index would map two KeyPackages onto the same
586/// per-index seed, and a duplicate reference would have the second upload
587/// entry overwrite the first's retained material, so both are rejected before
588/// any state is loaded or any operation generation is consumed.
589fn validate_key_package_infos(infos: &[KeyPackageInfo]) -> Result<(), VirtualClientsError> {
590    let mut seen_indices = std::collections::BTreeSet::new();
591    let mut seen_refs = std::collections::BTreeSet::new();
592    for info in infos {
593        if !seen_indices.insert(info.key_package_index) {
594            return Err(VirtualClientsError::DuplicateKeyPackageIndex(
595                info.key_package_index,
596            ));
597        }
598        if !seen_refs.insert(&info.key_package_ref) {
599            return Err(VirtualClientsError::DuplicateKeyPackageRef);
600        }
601    }
602    Ok(())
603}
604
605/// Build a [`KeyPackageUpload`] for `epoch_id` from a batch's `generation` and
606/// its [`KeyPackageInfo`] entries, filling `leaf_index` from the
607/// [`EmulationEpochState`] stored for that epoch.
608///
609/// The virtual client calls this after building a batch of KeyPackages with
610/// [`KeyPackageBuilder::build_vc_batch`] to assemble the message it hands to
611/// its sibling. `generation` is the single `key_package` operation generation
612/// the batch consumed.
613///
614/// Returns [`VirtualClientsError::MissingEmulationEpochState`] if no state is
615/// registered for `epoch_id`.
616///
617/// [`KeyPackageBuilder::build_vc_batch`]: crate::key_packages::KeyPackageBuilder::build_vc_batch
618pub fn assemble_vc_key_package_upload<Storage: crate::storage::StorageProvider>(
619    storage: &Storage,
620    epoch_id: EpochId,
621    generation: u32,
622    key_package_info: Vec<KeyPackageInfo>,
623) -> Result<KeyPackageUpload, VirtualClientsError> {
624    validate_key_package_infos(&key_package_info)?;
625    let state: EmulationEpochState = storage
626        .vc_emulation_epoch_state(&epoch_id)
627        .map_err(|e| {
628            log::error!("vc: load emulation epoch state in assemble upload failed: {e:?}");
629            VirtualClientsError::StorageError
630        })?
631        .ok_or(VirtualClientsError::MissingEmulationEpochState)?;
632    Ok(KeyPackageUpload {
633        epoch_id,
634        leaf_index: state.leaf_index,
635        generation,
636        key_package_info,
637    })
638}
639
640/// Process a [`KeyPackageUpload`] received from a sibling virtual client.
641///
642/// Derives the batch's single `key_package` operation secret once from the
643/// uploader's leaf ratchet at `(epoch_id, leaf_index, generation)`, then
644/// stores the advanced operation tree and one [`RetainedKeyPackageMaterial`]
645/// per [`KeyPackageInfo`] (keyed by the info's [`KeyPackageRef`]) in a single
646/// atomic batch write.
647///
648/// The batch operation secret is derived under the emulation ciphersuite (the
649/// operation tree's ciphersuite). Each per-KeyPackage seed is imported from
650/// it into the ciphersuite the upload names for this KeyPackage. The init and
651/// leaf-encryption keys are later derived from each seed under the same
652/// ciphersuite at Welcome time. The operation secret is dropped once all seeds
653/// are derived. The batch generation is consumed in the tree exactly once.
654pub fn process_vc_key_package_upload<Provider: OpenMlsProvider>(
655    provider: &Provider,
656    upload: &KeyPackageUpload,
657) -> Result<(), VirtualClientsError> {
658    use crate::components::vc_operation_tree::OperationSecretTree;
659    use openmls_traits::storage::StorageProvider as _;
660
661    validate_key_package_infos(&upload.key_package_info)?;
662
663    let storage = provider.storage();
664    let crypto = provider.crypto();
665
666    let state: EmulationEpochState = storage
667        .vc_emulation_epoch_state(&upload.epoch_id)
668        .map_err(|e| {
669            log::error!("vc: load emulation epoch state in process upload failed: {e:?}");
670            VirtualClientsError::StorageError
671        })?
672        .ok_or(VirtualClientsError::MissingEmulationEpochState)?;
673    let mut operation_tree: OperationSecretTree = storage
674        .vc_operation_tree(&upload.epoch_id)
675        .map_err(|e| {
676            log::error!("vc: load operation tree in process upload failed: {e:?}");
677            VirtualClientsError::StorageError
678        })?
679        .ok_or(VirtualClientsError::MissingOperationTree)?;
680    let emulation_ciphersuite = state.emulation_ciphersuite;
681
682    // The KeyPackage operation context is empty, matching `build_vc_batch`.
683    let operation_secret = operation_tree.derive_operation_secret(
684        crypto,
685        emulation_ciphersuite,
686        &upload.epoch_id,
687        upload.leaf_index,
688        VirtualClientOperationType::KeyPackage,
689        upload.generation,
690        b"",
691    )?;
692
693    let mut materials = Vec::with_capacity(upload.key_package_info.len());
694    for info in &upload.key_package_info {
695        let key_package_seed_secret = operation_secret.derive_key_package_seed_secret(
696            crypto,
697            info.cipher_suite,
698            info.key_package_index,
699        )?;
700        let material = RetainedKeyPackageMaterial {
701            epoch_id: upload.epoch_id.clone(),
702            leaf_index: upload.leaf_index,
703            generation: upload.generation,
704            key_package_ciphersuite: info.cipher_suite,
705            key_package_index: info.key_package_index,
706            key_package_seed_secret,
707        };
708        materials.push((info.key_package_ref.clone(), material));
709    }
710
711    storage
712        .write_retained_key_package_material_batch(&upload.epoch_id, &operation_tree, &materials)
713        .map_err(|e| {
714            log::error!("vc: persist batch key package material in process upload failed: {e:?}");
715            VirtualClientsError::StorageError
716        })?;
717    Ok(())
718}
719
720/// Material a sibling emulator derives to join a higher-level group via a
721/// virtual client's KeyPackage.
722///
723/// Carried from the first Welcome stage (where the init private key decrypts
724/// the group secrets, before the ratchet tree is available) into staging
725/// (where the derived `encryption_keypair` becomes the joiner's leaf keypair
726/// and the recorded `(epoch_id, leaf_index, generation, key_package_index)`
727/// validate the leaf found in the tree). The keys are derived from the
728/// per-KeyPackage seed pinned in [`RetainedKeyPackageMaterial`], not by
729/// re-walking the operation tree.
730#[derive(Debug)]
731pub(crate) struct VcWelcomeMaterial {
732    /// The [`KeyPackageRef`] the welcome's encrypted group secrets addressed.
733    pub(crate) key_package_ref: KeyPackageRef,
734    /// Emulation epoch the KeyPackage belongs to.
735    pub(crate) epoch_id: EpochId,
736    /// Uploader's emulation-group leaf index, identifying the operation
737    /// ratchet the batch generation was allocated from.
738    pub(crate) leaf_index: LeafNodeIndex,
739    /// Operation-ratchet generation consumed for the whole batch.
740    pub(crate) generation: u32,
741    /// Position of this KeyPackage within the batch.
742    pub(crate) key_package_index: u32,
743    /// Init private key derived from the seed, used to decrypt the encrypted
744    /// group secrets.
745    pub(crate) init_private_key: openmls_traits::types::HpkePrivateKey,
746    /// Init key the welcome encrypted group secrets are encrypted with.
747    pub(crate) init_key: InitKey,
748    /// Leaf encryption keypair derived from the seed, used as the joiner's
749    /// leaf keypair.
750    pub(crate) encryption_keypair: EncryptionKeyPair,
751}
752
753/// The emulation epoch an emulation group registered at one of its own group
754/// epochs, recorded by [`MlsGroup::register_vc_emulation_epoch`] so that a
755/// repeated call in the same group epoch returns the existing [`EpochId`]
756/// instead of consuming the forward-secure exporter again (the exporter is
757/// punctured by the first call and cannot be re-evaluated).
758///
759/// Not folded into [`VcEmulationBindings`]: bindings are carried forward to
760/// the new epoch when a merged commit installs no virtual-client leaf, so
761/// they cannot distinguish a registration in the current epoch from a
762/// carry-forward of an older one.
763///
764/// [`MlsGroup::register_vc_emulation_epoch`]: crate::group::MlsGroup::register_vc_emulation_epoch
765#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
766pub(crate) struct RegisteredVcEmulationEpoch {
767    /// The emulation group's own epoch at registration time.
768    pub(crate) group_epoch: crate::group::GroupEpoch,
769    /// The emulation epoch id derived by that registration.
770    pub(crate) epoch_id: EpochId,
771}
772
773/// Per-higher-level-group record of which emulation-group epoch produced the
774/// virtual-client LeafNode that was active at each recent epoch of that
775/// group.
776///
777/// Reuse guards must be resolved with the emulation epoch that was bound at
778/// the higher-level epoch a message was sent in, not the latest one: a
779/// delayed PrivateMessage from a past higher-level epoch has to be
780/// deprotected with the state that was active then. Entries are written at
781/// commit merge and retained for as many past epochs as the group's message
782/// secrets store keeps, since a binding is only useful while the matching
783/// message secrets still exist.
784#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
785pub struct VcEmulationBindings {
786    // In order of insertion, oldest at the front.
787    bindings: std::collections::VecDeque<(crate::group::GroupEpoch, EpochId)>,
788}
789
790impl VcEmulationBindings {
791    /// Look up the emulation epoch bound at the given higher-level epoch.
792    pub fn get(&self, epoch: crate::group::GroupEpoch) -> Option<&EpochId> {
793        for (bound_epoch, epoch_id) in &self.bindings {
794            if *bound_epoch == epoch {
795                return Some(epoch_id);
796            }
797        }
798        None
799    }
800
801    /// Record `epoch_id` as the binding for `epoch`, keeping at most
802    /// `max_entries` entries by dropping the oldest ones.
803    pub(crate) fn insert(
804        &mut self,
805        epoch: crate::group::GroupEpoch,
806        epoch_id: EpochId,
807        max_entries: usize,
808    ) {
809        self.bindings
810            .retain(|(bound_epoch, _)| *bound_epoch != epoch);
811        self.bindings.push_back((epoch, epoch_id));
812        while self.bindings.len() > max_entries {
813            self.bindings.pop_front();
814        }
815    }
816}
817
818/// Per-epoch secret from which the sender derives the AEAD key and nonce
819/// that wrap the [`DerivationInfoTbe`] in the leaf's `app_data_dictionary`
820/// entry, and the receiver the same pair to unwrap it:
821///
822/// ```text
823/// derivation_info_key = ExpandWithLabel(epoch_encryption_key, "key",
824///                                       encryption_key, AEAD.Nk)
825/// derivation_info_nonce = ExpandWithLabel(epoch_encryption_key, "nonce",
826///                                         encryption_key, AEAD.Nn)
827/// ```
828///
829/// where `encryption_key` is the serialized `encryption_key` field of the
830/// LeafNode carrying the derivation info. Every operation produces a fresh
831/// leaf encryption key, so each wrap uses a distinct key-nonce pair.
832/// Derived from the emulation group's `safe_export_secret(VC_COMPONENT_ID)`
833/// by [`MlsGroup::register_vc_emulation_epoch`].
834///
835/// [`MlsGroup::register_vc_emulation_epoch`]: crate::group::MlsGroup::register_vc_emulation_epoch
836#[derive(Debug, Serialize, Deserialize)]
837pub(crate) struct EpochEncryptionKey(Secret);
838
839impl EpochEncryptionKey {
840    /// Derive the AEAD key and nonce for one [`DerivationInfoTbe`] wrap,
841    /// using the serialized `encryption_key` of the carrying leaf as the
842    /// `ExpandWithLabel` context.
843    fn derive_key_nonce(
844        &self,
845        crypto: &impl OpenMlsCrypto,
846        ciphersuite: Ciphersuite,
847        leaf_encryption_key: &[u8],
848    ) -> Result<(Secret, Secret), VirtualClientsError> {
849        let key = self.0.kdf_expand_label(
850            crypto,
851            ciphersuite,
852            DERIVATION_INFO_KEY_LABEL,
853            leaf_encryption_key,
854            ciphersuite.aead_key_length(),
855        )?;
856        let nonce = self.0.kdf_expand_label(
857            crypto,
858            ciphersuite,
859            DERIVATION_INFO_NONCE_LABEL,
860            leaf_encryption_key,
861            ciphersuite.aead_nonce_length(),
862        )?;
863        Ok((key, nonce))
864    }
865}
866
867/// Per-emulation-epoch state persisted by
868/// [`MlsGroup::register_vc_emulation_epoch`] alongside the per-epoch
869/// operation secret tree, keyed by [`EpochId`]. Bundles everything the
870/// library needs to emit a VC commit for this epoch and to XOR application
871/// message nonces with deterministic reuse guards.
872///
873/// [`MlsGroup::register_vc_emulation_epoch`]:
874///     crate::group::MlsGroup::register_vc_emulation_epoch
875#[derive(Debug, Serialize, Deserialize)]
876pub struct EmulationEpochState {
877    /// The registering client's leaf index in the emulation group at
878    /// registration time. Sent in `DerivationInfoTbe` and used as the
879    /// sender's `leaf_index_e` in the reuse-guard derivation.
880    pub(crate) leaf_index: LeafNodeIndex,
881    pub(crate) epoch_encryption_key: EpochEncryptionKey,
882    pub(crate) reuse_guard_secret: ReuseGuardSecret,
883    /// Used to derive the per-message [`GenerationId`] handed to the DS, via
884    /// [`EmulationEpochState::derive_generation_id`].
885    pub(crate) generation_id_secret: GenerationIdSecret,
886    /// Number of leaves `N_e` in the emulation group at registration time.
887    pub(crate) emulation_group_size: TreeSize,
888    /// Ciphersuite of the emulation group at registration time. Used by
889    /// the reuse-guard derivation.
890    pub(crate) emulation_ciphersuite: Ciphersuite,
891}
892
893impl EmulationEpochState {
894    pub(crate) fn new(
895        leaf_index: LeafNodeIndex,
896        epoch_encryption_key: EpochEncryptionKey,
897        reuse_guard_secret: ReuseGuardSecret,
898        generation_id_secret: GenerationIdSecret,
899        emulation_group_size: TreeSize,
900        emulation_ciphersuite: Ciphersuite,
901    ) -> Self {
902        Self {
903            leaf_index,
904            epoch_encryption_key,
905            reuse_guard_secret,
906            generation_id_secret,
907            emulation_group_size,
908            emulation_ciphersuite,
909        }
910    }
911
912    /// Consume the state and return the fields needed by the
913    /// commit-builder / commit-processing paths.
914    pub(crate) fn into_parts(self) -> (LeafNodeIndex, EpochEncryptionKey, Ciphersuite) {
915        (
916            self.leaf_index,
917            self.epoch_encryption_key,
918            self.emulation_ciphersuite,
919        )
920    }
921
922    /// Derive the [`GenerationId`] for an application message sent in
923    /// `group_id` at `epoch` with ratchet `generation`. The
924    /// [`PrivateMessageContext`] is assembled from these inputs and the
925    /// emulation epoch's [`GenerationIdSecret`], using the emulation group's
926    /// ciphersuite.
927    pub(crate) fn derive_generation_id(
928        &self,
929        crypto: &impl OpenMlsCrypto,
930        group_id: &GroupId,
931        epoch: GroupEpoch,
932        generation: u32,
933        ratchet_type: RatchetType,
934    ) -> Result<GenerationId, VirtualClientsError> {
935        let context = PrivateMessageContext {
936            group_id: VLByteSlice(group_id.as_slice()),
937            epoch: epoch.as_u64(),
938            generation,
939            ratchet_type,
940        };
941        self.generation_id_secret
942            .derive_generation_id(crypto, self.emulation_ciphersuite, &context)
943    }
944
945    /// Borrow the per-message inputs the framing layer needs to derive
946    /// the PRP key and pick `x` for a reuse guard.
947    pub(crate) fn reuse_guard_inputs(&self) -> crate::framing::EmulatorReuseGuardCtx<'_> {
948        crate::framing::EmulatorReuseGuardCtx {
949            reuse_guard_secret: &self.reuse_guard_secret,
950            emulation_ciphersuite: self.emulation_ciphersuite,
951            emulation_group_size: self.emulation_group_size,
952            emulation_leaf_index: self.leaf_index,
953        }
954    }
955}
956
957/// Per-operation secret from which the material for a single virtual-clients
958/// operation (commit path, key package, application message) is derived.
959/// Produced by the per-epoch Virtual Client Operation Secret Tree
960/// ([`OperationSecretTree`]). Sender and receiver derive the same value
961/// from the same per-epoch state.
962///
963/// [`OperationSecretTree`]: crate::components::vc_operation_tree::OperationSecretTree
964#[derive(Debug, Serialize, Deserialize)]
965pub struct OperationSecret(Secret);
966
967impl From<Secret> for OperationSecret {
968    fn from(secret: Secret) -> Self {
969        Self(secret)
970    }
971}
972
973/// Imports a secret from the emulation group's ciphersuite to the target ciphersuite.
974///
975/// The import MUST be performed even when the emulation group and target use the same ciphersuite.
976fn import_secret(
977    crypto: &impl OpenMlsCrypto,
978    target_ciphersuite: Ciphersuite,
979    source_secret: &Secret,
980    label: &str,
981    context: &[u8],
982) -> Result<Secret, CryptoError> {
983    let salt = Secret::from_slice(&[]);
984    let target_prk = salt.hkdf_extract(crypto, target_ciphersuite, source_secret)?;
985    target_prk.kdf_expand_label(
986        crypto,
987        target_ciphersuite,
988        label,
989        context,
990        target_ciphersuite.hash_length(),
991    )
992}
993
994impl OperationSecret {
995    /// Test-only accessor for comparing derived operation secrets.
996    #[cfg(test)]
997    pub(crate) fn as_slice(&self) -> &[u8] {
998        self.0.as_slice()
999    }
1000
1001    /// Derive the `target_operation_secret` of a `leaf_node` operation: this
1002    /// operation secret imported into the higher-level group's ciphersuite:
1003    ///
1004    /// ```text
1005    /// target_operation_secret = ImportSecret(operation_secret,
1006    ///                                        "vc target operation",
1007    ///                                        TargetOperationContext)
1008    /// ```
1009    ///
1010    /// The context binds the target ciphersuite and the higher-level group's
1011    /// `group_id`, so one operation secret yields independent path material
1012    /// per target group. The commit path's encryption-key and path-generation
1013    /// secrets are derived from the returned [`TargetOperationSecret`], not
1014    /// from the operation secret directly. The committing emulator and the
1015    /// sibling recreating the commit derive the same value.
1016    pub(crate) fn derive_target_operation_secret(
1017        &self,
1018        crypto: &impl OpenMlsCrypto,
1019        target_ciphersuite: Ciphersuite,
1020        group_id: &GroupId,
1021    ) -> Result<TargetOperationSecret, VirtualClientsError> {
1022        let context = TargetOperationContext {
1023            cipher_suite: target_ciphersuite,
1024            group_id: VLByteSlice(group_id.as_slice()),
1025        }
1026        .tls_serialize_detached()?;
1027        let secret = import_secret(
1028            crypto,
1029            target_ciphersuite,
1030            &self.0,
1031            TARGET_OPERATION_LABEL,
1032            &context,
1033        )?;
1034        Ok(TargetOperationSecret(secret))
1035    }
1036
1037    /// Derive the per-KeyPackage seed secret for the KeyPackage at
1038    /// `key_package_index` within this operation's batch:
1039    ///
1040    /// ```text
1041    /// key_package_seed_secret = ImportSecret(operation_secret,
1042    ///                                        "vc key package seed",
1043    ///                                        KeyPackageSeedContext)
1044    /// ```
1045    ///
1046    /// The KeyPackage's init and leaf-encryption keys are then derived from the
1047    /// returned [`KeyPackageSeedSecret`], not from the operation secret
1048    /// directly, so a single `key_package` operation secret can cover a batch
1049    /// of KeyPackages with distinct key material.
1050    pub(crate) fn derive_key_package_seed_secret(
1051        &self,
1052        crypto: &impl OpenMlsCrypto,
1053        target_ciphersuite: Ciphersuite,
1054        key_package_index: u32,
1055    ) -> Result<KeyPackageSeedSecret, VirtualClientsError> {
1056        let context = KeyPackageSeedContext {
1057            cipher_suite: target_ciphersuite,
1058            key_package_index,
1059        }
1060        .tls_serialize_detached()?;
1061        let seed = import_secret(
1062            crypto,
1063            target_ciphersuite,
1064            &self.0,
1065            KEY_PACKAGE_SEED_LABEL,
1066            &context,
1067        )?;
1068        Ok(KeyPackageSeedSecret(seed))
1069    }
1070}
1071
1072/// `ExpandWithLabel` context for [`OperationSecret::derive_key_package_seed_secret`]
1073/// (mls-virtual-clients draft):
1074///
1075/// ```text
1076/// struct {
1077///   CipherSuite cipher_suite;
1078///   uint32 key_package_index;
1079/// } KeyPackageSeedContext
1080/// ```
1081///
1082/// Only ever serialized as a derivation context, never parsed back, so it
1083/// needs serialization only.
1084#[derive(Debug, TlsSize, TlsSerialize)]
1085struct KeyPackageSeedContext {
1086    cipher_suite: Ciphersuite,
1087    key_package_index: u32,
1088}
1089
1090/// Per-KeyPackage seed secret from which a single KeyPackage's init and
1091/// leaf-encryption keys are derived. Produced by
1092/// `OperationSecret::derive_key_package_seed_secret` for one index within a
1093/// `key_package` operation's batch. Persisted in [`RetainedKeyPackageMaterial`]
1094/// so the Welcome path can rederive the keys without re-walking the operation
1095/// tree.
1096#[derive(Debug, Serialize, Deserialize)]
1097pub struct KeyPackageSeedSecret(Secret);
1098
1099impl KeyPackageSeedSecret {
1100    pub(crate) fn derive_init_key_secret(
1101        &self,
1102        crypto: &impl OpenMlsCrypto,
1103        ciphersuite: Ciphersuite,
1104    ) -> Result<InitKeySecret, VirtualClientsError> {
1105        let init_key_secret = self.0.derive_secret(crypto, ciphersuite, INIT_KEY_LABEL)?;
1106        Ok(InitKeySecret(init_key_secret))
1107    }
1108
1109    pub(crate) fn derive_encryption_key_secret(
1110        &self,
1111        crypto: &impl OpenMlsCrypto,
1112        ciphersuite: Ciphersuite,
1113    ) -> Result<EncryptionKeySecret, VirtualClientsError> {
1114        let encryption_key_secret =
1115            self.0
1116                .derive_secret(crypto, ciphersuite, ENCRYPTION_KEY_LABEL)?;
1117        Ok(EncryptionKeySecret(encryption_key_secret))
1118    }
1119
1120    /// Derive the epoch-0 `epoch_secret` for a virtual-client-created group:
1121    ///
1122    /// ```text
1123    /// epoch_secret = DeriveSecret(key_package_seed_secret, "Group Creation")
1124    /// ```
1125    ///
1126    /// `ciphersuite` is the created (higher-level) group's ciphersuite, under
1127    /// which the resulting `epoch_secret` seeds the epoch key schedule. Both
1128    /// the creator and a reconstructing sibling derive it from the same seed,
1129    /// so the epoch secret never travels on the wire.
1130    pub(crate) fn derive_group_creation_secret(
1131        &self,
1132        crypto: &impl OpenMlsCrypto,
1133        ciphersuite: Ciphersuite,
1134    ) -> Result<Secret, VirtualClientsError> {
1135        Ok(self
1136            .0
1137            .derive_secret(crypto, ciphersuite, GROUP_CREATION_LABEL)?)
1138    }
1139}
1140
1141pub(crate) struct EncryptionKeySecret(Secret);
1142
1143impl EncryptionKeySecret {
1144    pub(crate) fn generate_encryption_key_pair(
1145        &self,
1146        crypto: &impl OpenMlsCrypto,
1147        ciphersuite: Ciphersuite,
1148    ) -> Result<EncryptionKeyPair, VirtualClientsError> {
1149        let hpke_config = ciphersuite.hpke_config();
1150        let key_pair = crypto.derive_hpke_keypair(hpke_config, self.0.as_slice())?;
1151        Ok(EncryptionKeyPair::from(key_pair))
1152    }
1153}
1154
1155pub(crate) struct InitKeySecret(Secret);
1156
1157impl InitKeySecret {
1158    pub(crate) fn generate_init_key_pair(
1159        &self,
1160        crypto: &impl OpenMlsCrypto,
1161        ciphersuite: Ciphersuite,
1162    ) -> Result<openmls_traits::types::HpkeKeyPair, VirtualClientsError> {
1163        let hpke_config = ciphersuite.hpke_config();
1164        let key_pair = crypto.derive_hpke_keypair(hpke_config, self.0.as_slice())?;
1165        Ok(key_pair)
1166    }
1167}
1168
1169pub(crate) struct PathGenerationSecret(Secret);
1170
1171impl From<PathGenerationSecret> for PathSecret {
1172    fn from(value: PathGenerationSecret) -> Self {
1173        value.0.into()
1174    }
1175}
1176
1177/// What virtual-clients operation a per-operation secret is being derived
1178/// for (mls-virtual-clients draft `VirtualClientOperationType`). Mixed into
1179/// the `OperationContext` of every operation-secret derivation so that
1180/// secrets derived for different operations cannot collide even if the other
1181/// fields happen to match.
1182///
1183/// The operation type does not travel on the wire. Receivers infer it from
1184/// the carrying LeafNode's `leaf_node_source`: `key_package` maps to
1185/// [`KeyPackage`](Self::KeyPackage), `update` and `commit` map to
1186/// [`LeafNode`](Self::LeafNode).
1187///
1188/// Only `LeafNode` is wired into a sender path today (see `apply_vc_emulation`
1189/// in the commit builder). `KeyPackage` and `Application` are reserved
1190/// variants that a follow-up PR will emit, once the KeyPackage and
1191/// application-message operation paths exist.
1192#[derive(Debug, Clone, Copy, PartialEq, Eq, TlsSize, TlsSerialize, TlsDeserializeBytes)]
1193#[repr(u8)]
1194pub enum VirtualClientOperationType {
1195    /// Derivation of KeyPackage material for the virtual client.
1196    KeyPackage = 1,
1197    /// Derivation of LeafNode material for the virtual client (e.g. the
1198    /// leaf carried by a commit).
1199    LeafNode = 2,
1200    /// Derivation of application-message material for the virtual client.
1201    Application = 3,
1202}
1203
1204/// The external init secret carried by an external-commit LeafNode's
1205/// `DerivationInfoTBE` (mls-virtual-clients draft):
1206///
1207/// ```text
1208/// struct { opaque init_secret<V>; } ExternalInitSecret;
1209/// ```
1210///
1211/// It is the `init_secret` produced by external initialization
1212/// ({{Section 8.3 of RFC9420}}). A sibling emulator client processing the
1213/// external commit uses it as the new epoch's external init secret instead of
1214/// decapsulating from the previous epoch's `external_secret`, which it may not
1215/// hold.
1216#[derive(Clone, PartialEq, Eq, TlsSize, TlsSerialize, TlsDeserializeBytes)]
1217pub(crate) struct ExternalInitSecret(SecretVLByteVec);
1218
1219impl std::fmt::Debug for ExternalInitSecret {
1220    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1221        f.debug_struct("ExternalInitSecret")
1222            .field("init_secret", &"<redacted>")
1223            .finish()
1224    }
1225}
1226
1227impl ExternalInitSecret {
1228    pub(crate) fn from_slice(bytes: &[u8]) -> Self {
1229        Self(bytes.to_vec().into())
1230    }
1231
1232    pub(crate) fn as_slice(&self) -> &[u8] {
1233        self.0.as_slice()
1234    }
1235}
1236
1237/// ```text
1238/// struct {
1239///   CipherSuite cipher_suite;
1240///   opaque group_id<V>;
1241/// } TargetOperationContext
1242/// ```
1243#[derive(Debug, TlsSize, TlsSerialize)]
1244struct TargetOperationContext<'a> {
1245    cipher_suite: Ciphersuite,
1246    group_id: VLByteSlice<'a>,
1247}
1248
1249/// A leaf node operation secret imported into the higher-level group's ciphersuite.
1250///
1251/// Must be immediately deleted after the encryption key/path generation secrets are derived.
1252#[derive(Debug)]
1253pub(crate) struct TargetOperationSecret(Secret);
1254
1255impl TargetOperationSecret {
1256    pub(crate) fn derive_encryption_key_secret(
1257        &self,
1258        crypto: &impl OpenMlsCrypto,
1259        ciphersuite: Ciphersuite,
1260    ) -> Result<EncryptionKeySecret, VirtualClientsError> {
1261        let encryption_key_secret =
1262            self.0
1263                .derive_secret(crypto, ciphersuite, ENCRYPTION_KEY_LABEL)?;
1264        Ok(EncryptionKeySecret(encryption_key_secret))
1265    }
1266
1267    pub(crate) fn derive_path_generation_secret(
1268        &self,
1269        crypto: &impl OpenMlsCrypto,
1270        ciphersuite: Ciphersuite,
1271    ) -> Result<PathGenerationSecret, VirtualClientsError> {
1272        let path_generation_secret =
1273            self.0
1274                .derive_secret(crypto, ciphersuite, PATH_GENERATION_LABEL)?;
1275        Ok(PathGenerationSecret(path_generation_secret))
1276    }
1277}
1278
1279/// What a receiver derives from a sibling virtual client's commit in order to
1280/// recreate it: the emulation `epoch_id` the commit binds to, the per-commit
1281/// `operation_secret` the path is rederived from, and, for an external commit,
1282/// the carried `external_init_secret` (`None` for a regular commit).
1283///
1284/// Produced by `MlsGroup::load_vc_commit_material` and threaded into commit
1285/// staging as a single `Option`: either all three are present (a sibling VC
1286/// commit) or none are.
1287#[derive(Debug)]
1288pub(crate) struct VcCommitMaterial {
1289    /// Emulation epoch the commit's derivation info references.
1290    pub(crate) epoch_id: EpochId,
1291    /// Per-commit operation secret the receiver rederives the path from.
1292    pub(crate) operation_secret: OperationSecret,
1293    /// External init secret carried by an external commit, `None` otherwise.
1294    pub(crate) external_init_secret: Option<ExternalInitSecret>,
1295}
1296
1297/// AEAD plaintext attached to the leaf via the VC component
1298/// (mls-virtual-clients draft):
1299///
1300/// ```text
1301/// struct {
1302///   uint32 leaf_index;
1303///   uint32 generation;
1304///   select (LeafNode.leaf_node_source) {
1305///     case key_package: uint32 key_package_index;
1306///     case update:      struct{};
1307///     case commit:      optional<ExternalInitSecret> external_init_secret;
1308///   };
1309/// } DerivationInfoTBE
1310/// ```
1311///
1312/// `leaf_index` is the *emulation*-group leaf index of the sending virtual
1313/// client, *not* the leaf index in the group that carries this commit.
1314/// `generation` is the operation-ratchet generation the sender consumed for
1315/// this operation. `key_package_index`, present only for the `KeyPackage`
1316/// variant, is the KeyPackage's position within its `key_package` operation
1317/// batch. `external_init_secret`, present only for the commit variant, carries
1318/// the external init secret of an external commit (`Some`) and is absent
1319/// (`None`) for a regular commit.
1320#[derive(Debug, PartialEq, Eq)]
1321pub(crate) enum DerivationInfoTbe {
1322    /// Carried by `update` and `commit` leaves. No `key_package_index`. The
1323    /// codec treats the `LeafNode` operation type as the `commit` case (the
1324    /// only LeafNode-source leaf emitted today). `update`-proposal leaves are
1325    /// deferred and would need their own (field-less) codec branch.
1326    LeafNode {
1327        leaf_index: LeafNodeIndex,
1328        generation: u32,
1329        /// `Some` for an external commit, `None` for a regular commit.
1330        external_init_secret: Option<ExternalInitSecret>,
1331    },
1332    /// Carried by `key_package` leaves. Adds the position within the batch.
1333    KeyPackage {
1334        leaf_index: LeafNodeIndex,
1335        generation: u32,
1336        key_package_index: u32,
1337    },
1338}
1339
1340impl DerivationInfoTbe {
1341    /// The emulation-group leaf index of the sending virtual client.
1342    pub(crate) fn leaf_index(&self) -> LeafNodeIndex {
1343        match self {
1344            Self::LeafNode { leaf_index, .. } | Self::KeyPackage { leaf_index, .. } => *leaf_index,
1345        }
1346    }
1347
1348    /// The operation-ratchet generation the sender consumed.
1349    pub(crate) fn generation(&self) -> u32 {
1350        match self {
1351            Self::LeafNode { generation, .. } | Self::KeyPackage { generation, .. } => *generation,
1352        }
1353    }
1354
1355    /// The external init secret carried by an external-commit LeafNode, if any.
1356    /// Always `None` for `KeyPackage` and for regular (non-external) commits.
1357    pub(crate) fn external_init_secret(&self) -> Option<&ExternalInitSecret> {
1358        match self {
1359            Self::LeafNode {
1360                external_init_secret,
1361                ..
1362            } => external_init_secret.as_ref(),
1363            Self::KeyPackage { .. } => None,
1364        }
1365    }
1366
1367    /// Serialize the variant's fields in order, with no variant tag, matching
1368    /// the `DerivationInfoTBE` select. The TLS derive macros cannot express a
1369    /// tagless select, so this codec is written by hand.
1370    fn tls_serialize_detached(&self) -> Result<Vec<u8>, tls_codec::Error> {
1371        match self {
1372            Self::LeafNode {
1373                leaf_index,
1374                generation,
1375                external_init_secret,
1376            } => {
1377                let mut out = Vec::with_capacity(
1378                    leaf_index.tls_serialized_len()
1379                        + generation.tls_serialized_len()
1380                        + external_init_secret.tls_serialized_len(),
1381                );
1382                leaf_index.tls_serialize(&mut out)?;
1383                generation.tls_serialize(&mut out)?;
1384                external_init_secret.tls_serialize(&mut out)?;
1385                Ok(out)
1386            }
1387            Self::KeyPackage {
1388                leaf_index,
1389                generation,
1390                key_package_index,
1391            } => {
1392                let mut out = Vec::with_capacity(
1393                    leaf_index.tls_serialized_len()
1394                        + generation.tls_serialized_len()
1395                        + key_package_index.tls_serialized_len(),
1396                );
1397                leaf_index.tls_serialize(&mut out)?;
1398                generation.tls_serialize(&mut out)?;
1399                key_package_index.tls_serialize(&mut out)?;
1400                Ok(out)
1401            }
1402        }
1403    }
1404
1405    /// Deserialize the tagless select for the given operation type. The
1406    /// operation type stands in for the carrying leaf's `leaf_node_source`:
1407    /// [`KeyPackage`](VirtualClientOperationType::KeyPackage) parses the
1408    /// `KeyPackage` variant, [`LeafNode`](VirtualClientOperationType::LeafNode)
1409    /// the `LeafNode` variant. The plaintext must be consumed exactly.
1410    fn deserialize_for_operation(
1411        bytes: &[u8],
1412        operation_type: VirtualClientOperationType,
1413    ) -> Result<Self, VirtualClientsError> {
1414        let (leaf_index, rest) = LeafNodeIndex::tls_deserialize_bytes(bytes)?;
1415        let (generation, rest) = u32::tls_deserialize_bytes(rest)?;
1416        let (tbe, rest) = match operation_type {
1417            VirtualClientOperationType::KeyPackage => {
1418                let (key_package_index, rest) = u32::tls_deserialize_bytes(rest)?;
1419                (
1420                    Self::KeyPackage {
1421                        leaf_index,
1422                        generation,
1423                        key_package_index,
1424                    },
1425                    rest,
1426                )
1427            }
1428            // The `LeafNode` operation type is the `commit` case: it carries an
1429            // `optional<ExternalInitSecret>`. (`update`-proposal leaves are
1430            // deferred and would decode a field-less body instead.)
1431            VirtualClientOperationType::LeafNode => {
1432                let (external_init_secret, rest) =
1433                    Option::<ExternalInitSecret>::tls_deserialize_bytes(rest)?;
1434                (
1435                    Self::LeafNode {
1436                        leaf_index,
1437                        generation,
1438                        external_init_secret,
1439                    },
1440                    rest,
1441                )
1442            }
1443            VirtualClientOperationType::Application => {
1444                return Err(VirtualClientsError::DerivationInfoMalformed);
1445            }
1446        };
1447        if !rest.is_empty() {
1448            return Err(VirtualClientsError::DerivationInfoMalformed);
1449        }
1450        Ok(tbe)
1451    }
1452}
1453
1454/// Load the [`EmulationEpochState`] and [`OperationSecretTree`] for `epoch_id`,
1455/// mapping a missing entry to the matching `Missing*` error. Callers convert the
1456/// returned [`VirtualClientsError`] into their own error type.
1457///
1458/// [`OperationSecretTree`]: crate::components::vc_operation_tree::OperationSecretTree
1459pub(crate) fn load_vc_epoch_state_and_tree<Provider: OpenMlsProvider>(
1460    provider: &Provider,
1461    epoch_id: &EpochId,
1462) -> Result<
1463    (
1464        EmulationEpochState,
1465        crate::components::vc_operation_tree::OperationSecretTree,
1466    ),
1467    VirtualClientsError,
1468> {
1469    use openmls_traits::storage::StorageProvider as _;
1470
1471    let storage = provider.storage();
1472    let state = storage
1473        .vc_emulation_epoch_state(epoch_id)
1474        .map_err(|e| {
1475            log::error!("vc: load emulation epoch state failed: {e:?}");
1476            VirtualClientsError::StorageError
1477        })?
1478        .ok_or(VirtualClientsError::MissingEmulationEpochState)?;
1479    let operation_tree = storage
1480        .vc_operation_tree(epoch_id)
1481        .map_err(|e| {
1482            log::error!("vc: load operation tree failed: {e:?}");
1483            VirtualClientsError::StorageError
1484        })?
1485        .ok_or(VirtualClientsError::MissingOperationTree)?;
1486    Ok((state, operation_tree))
1487}
1488
1489/// Verify that the effective leaf about to carry a VC derivation-info entry
1490/// declares `AppDataDictionary` and lists [`VC_COMPONENT_ID`] in its
1491/// `AppComponents` entry, and return the resolved `AppDataDictionary`.
1492///
1493/// `caller_capabilities` and `caller_extensions` are the leaf parameters the
1494/// caller supplied for this operation. `current_leaf` is the leaf being
1495/// replaced, or `None` when there is none (a fresh KeyPackage, or an external
1496/// commit). The caller's `AppDataDictionary` is merged over the current
1497/// leaf's, with the caller winning on duplicate component ids, so injecting
1498/// the VC derivation-info preserves the `AppComponents` entry across
1499/// operations.
1500pub(crate) fn resolve_vc_leaf_dictionary(
1501    caller_capabilities: Option<&crate::treesync::node::leaf_node::Capabilities>,
1502    caller_extensions: Option<
1503        &crate::extensions::Extensions<crate::treesync::node::leaf_node::LeafNode>,
1504    >,
1505    current_leaf: Option<&crate::treesync::node::leaf_node::LeafNode>,
1506) -> Result<crate::extensions::AppDataDictionary, VirtualClientsError> {
1507    use crate::{
1508        component::{ComponentId, ComponentType},
1509        extensions::ExtensionType,
1510    };
1511    use tls_codec::DeserializeBytes as _;
1512
1513    let supports_app_data_dictionary = match caller_capabilities {
1514        Some(c) => c.extensions().contains(&ExtensionType::AppDataDictionary),
1515        None => current_leaf
1516            .map(|leaf| {
1517                leaf.capabilities()
1518                    .extensions()
1519                    .contains(&ExtensionType::AppDataDictionary)
1520            })
1521            .unwrap_or(false),
1522    };
1523    if !supports_app_data_dictionary {
1524        return Err(VirtualClientsError::AppDataDictionaryNotSupported);
1525    }
1526
1527    let mut resolved_dictionary = current_leaf
1528        .and_then(|leaf| leaf.extensions().app_data_dictionary())
1529        .map(|ext| ext.dictionary().clone())
1530        .unwrap_or_default();
1531    if let Some(caller_dict) = caller_extensions.and_then(|exts| exts.app_data_dictionary()) {
1532        for entry in caller_dict.dictionary().entries() {
1533            resolved_dictionary.insert(entry.id(), entry.data().to_vec());
1534        }
1535    }
1536
1537    let app_components_bytes = resolved_dictionary
1538        .get(&ComponentId::from(ComponentType::AppComponents))
1539        .map(<[u8]>::to_vec);
1540    let Some(app_components_bytes) = app_components_bytes else {
1541        return Err(VirtualClientsError::VcComponentNotListed);
1542    };
1543
1544    // The AppComponents body is `ComponentID supported_components<V>`, i.e.
1545    // a TLS-encoded variable-length vector of u16.
1546    let supported_components = Vec::<u16>::tls_deserialize_exact_bytes(&app_components_bytes)
1547        .map_err(|e| {
1548            log::error!("vc: AppComponents body failed to deserialize: {e:?}");
1549            VirtualClientsError::VcComponentNotListed
1550        })?;
1551    if !supported_components.contains(&VC_COMPONENT_ID) {
1552        return Err(VirtualClientsError::VcComponentNotListed);
1553    }
1554
1555    Ok(resolved_dictionary)
1556}
1557
1558/// Merge a virtual-clients derivation-info blob into `resolved_dictionary`
1559/// under [`VC_COMPONENT_ID`] and build the resulting leaf-node extensions.
1560///
1561/// Every other component id in `resolved_dictionary` (notably `AppComponents`)
1562/// is preserved, as is every non-`AppDataDictionary` extension the caller
1563/// supplied in `caller_extensions`. The rebuilt dictionary replaces any
1564/// `AppDataDictionary` entry already in that list.
1565pub(crate) fn merge_vc_derivation_info(
1566    caller_extensions: Option<
1567        &crate::extensions::Extensions<crate::treesync::node::leaf_node::LeafNode>,
1568    >,
1569    mut resolved_dictionary: crate::extensions::AppDataDictionary,
1570    derivation_info_bytes: Vec<u8>,
1571) -> Result<
1572    crate::extensions::Extensions<crate::treesync::node::leaf_node::LeafNode>,
1573    crate::error::LibraryError,
1574> {
1575    use crate::extensions::{AppDataDictionaryExtension, Extension, Extensions};
1576
1577    resolved_dictionary.insert(VC_COMPONENT_ID, derivation_info_bytes);
1578    let vc_extension =
1579        Extension::AppDataDictionary(AppDataDictionaryExtension::new(resolved_dictionary));
1580
1581    let other_extensions = caller_extensions
1582        .map(|exts| {
1583            exts.iter()
1584                .filter(|ext| !matches!(ext, Extension::AppDataDictionary(_)))
1585                .cloned()
1586                .collect::<Vec<_>>()
1587        })
1588        .unwrap_or_default();
1589    let new_extensions: Vec<Extension> = other_extensions
1590        .into_iter()
1591        .chain(std::iter::once(vc_extension))
1592        .collect();
1593    Extensions::from_vec(new_extensions)
1594        .map_err(|_| crate::error::LibraryError::custom("Failed to build VC leaf-node extensions"))
1595}
1596
1597#[cfg(test)]
1598mod tests {
1599    use super::*;
1600    use openmls_rust_crypto::{MemoryStorage, OpenMlsRustCrypto};
1601    use openmls_traits::{
1602        random::OpenMlsRand,
1603        storage::{StorageProvider, CURRENT_VERSION},
1604        OpenMlsProvider,
1605    };
1606
1607    const CIPHERSUITE: Ciphersuite = Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519;
1608
1609    /// Register a full `EmulationEpochState` and a matching
1610    /// `OperationSecretTree` for a fresh epoch, returning the derived
1611    /// `EpochId` and the leaf index it was registered with.
1612    fn register_epoch_state(provider: &OpenMlsRustCrypto, leaf_index: LeafNodeIndex) -> EpochId {
1613        use crate::components::vc_operation_tree::OperationSecretTree;
1614
1615        let emulator = EmulatorEpochSecret::new(
1616            &provider
1617                .rand()
1618                .random_vec(CIPHERSUITE.hash_length())
1619                .expect("randomness"),
1620        );
1621        let epoch_id = emulator
1622            .derive_epoch_id(provider.crypto(), CIPHERSUITE)
1623            .expect("derive epoch id");
1624        let epoch_encryption_key = emulator
1625            .derive_epoch_encryption_key(provider.crypto(), CIPHERSUITE)
1626            .expect("derive epoch encryption key");
1627        let reuse_guard_secret = emulator
1628            .derive_reuse_guard_secret(provider.crypto(), CIPHERSUITE)
1629            .expect("derive reuse guard secret");
1630        let generation_id_secret = emulator
1631            .derive_generation_id_secret(provider.crypto(), CIPHERSUITE)
1632            .expect("derive generation id secret");
1633        let epoch_base_secret = emulator
1634            .derive_epoch_base_secret(provider.crypto(), CIPHERSUITE)
1635            .expect("derive epoch base secret");
1636        let emulation_group_size = TreeSize::new(2);
1637        let state = EmulationEpochState::new(
1638            leaf_index,
1639            epoch_encryption_key,
1640            reuse_guard_secret,
1641            generation_id_secret,
1642            emulation_group_size,
1643            CIPHERSUITE,
1644        );
1645        <MemoryStorage as StorageProvider<CURRENT_VERSION>>::write_vc_emulation_epoch_state(
1646            provider.storage(),
1647            &epoch_id,
1648            &state,
1649        )
1650        .expect("write emulation epoch state");
1651        let operation_tree = OperationSecretTree::new(epoch_base_secret, emulation_group_size);
1652        <MemoryStorage as StorageProvider<CURRENT_VERSION>>::write_vc_operation_tree(
1653            provider.storage(),
1654            &epoch_id,
1655            &operation_tree,
1656        )
1657        .expect("write operation tree");
1658        epoch_id
1659    }
1660
1661    /// The assembly helper fills `leaf_index` from the registered
1662    /// `EmulationEpochState` for the epoch.
1663    #[test]
1664    fn assemble_upload_reads_leaf_index_from_state() {
1665        let provider = OpenMlsRustCrypto::default();
1666        let leaf_index = LeafNodeIndex::new(5);
1667        let epoch_id = register_epoch_state(&provider, leaf_index);
1668        let infos = vec![
1669            KeyPackageInfo {
1670                key_package_ref: KeyPackageRef::from_slice(b"kp-ref-a"),
1671                cipher_suite: CIPHERSUITE,
1672                key_package_index: 0,
1673            },
1674            KeyPackageInfo {
1675                key_package_ref: KeyPackageRef::from_slice(b"kp-ref-b"),
1676                cipher_suite: CIPHERSUITE,
1677                key_package_index: 1,
1678            },
1679        ];
1680
1681        let upload = assemble_vc_key_package_upload(provider.storage(), epoch_id.clone(), 4, infos)
1682            .expect("assemble upload");
1683
1684        assert_eq!(upload.epoch_id, epoch_id);
1685        assert_eq!(upload.leaf_index, leaf_index);
1686        assert_eq!(upload.generation, 4);
1687        assert_eq!(upload.key_package_info.len(), 2);
1688    }
1689
1690    /// Assembling for an unregistered epoch fails with
1691    /// `MissingEmulationEpochState`.
1692    #[test]
1693    fn assemble_upload_without_state_fails() {
1694        let provider = OpenMlsRustCrypto::default();
1695        let epoch_id = EpochId(b"unregistered-epoch".to_vec().into());
1696        let err = assemble_vc_key_package_upload(provider.storage(), epoch_id, 0, Vec::new())
1697            .expect_err("assemble must fail without registered state");
1698        assert_eq!(err, VirtualClientsError::MissingEmulationEpochState);
1699    }
1700
1701    /// `process_vc_key_package_upload` stores one material entry per info,
1702    /// readable back via `retained_key_package_material` keyed by the
1703    /// KeyPackage reference, each carrying its own batch index.
1704    #[test]
1705    fn process_upload_stores_records() {
1706        let provider = OpenMlsRustCrypto::default();
1707        let leaf_index = LeafNodeIndex::new(0);
1708        let epoch_id = register_epoch_state(&provider, leaf_index);
1709        let ref_a = KeyPackageRef::from_slice(b"kp-ref-a");
1710        let ref_b = KeyPackageRef::from_slice(b"kp-ref-b");
1711        let upload = KeyPackageUpload {
1712            epoch_id: epoch_id.clone(),
1713            leaf_index,
1714            generation: 0,
1715            key_package_info: vec![
1716                KeyPackageInfo {
1717                    key_package_ref: ref_a.clone(),
1718                    cipher_suite: CIPHERSUITE,
1719                    key_package_index: 0,
1720                },
1721                KeyPackageInfo {
1722                    key_package_ref: ref_b.clone(),
1723                    cipher_suite: CIPHERSUITE,
1724                    key_package_index: 1,
1725                },
1726            ],
1727        };
1728
1729        process_vc_key_package_upload(&provider, &upload).expect("process upload");
1730
1731        let material_a: RetainedKeyPackageMaterial = <MemoryStorage as StorageProvider<
1732            CURRENT_VERSION,
1733        >>::retained_key_package_material(
1734            provider.storage(), &ref_a
1735        )
1736        .expect("read material a")
1737        .expect("material a present");
1738        assert_eq!(material_a.epoch_id, epoch_id);
1739        assert_eq!(material_a.leaf_index, leaf_index);
1740        assert_eq!(material_a.generation, 0);
1741        assert_eq!(material_a.key_package_index, 0);
1742        assert_eq!(material_a.key_package_ciphersuite, CIPHERSUITE);
1743
1744        let material_b: RetainedKeyPackageMaterial = <MemoryStorage as StorageProvider<
1745            CURRENT_VERSION,
1746        >>::retained_key_package_material(
1747            provider.storage(), &ref_b
1748        )
1749        .expect("read material b")
1750        .expect("material b present");
1751        assert_eq!(material_b.epoch_id, epoch_id);
1752        assert_eq!(material_b.leaf_index, leaf_index);
1753        assert_eq!(material_b.generation, 0);
1754        assert_eq!(material_b.key_package_index, 1);
1755        assert_eq!(material_b.key_package_ciphersuite, CIPHERSUITE);
1756    }
1757
1758    /// `delete_key_package` removes the associated retained VC material.
1759    #[test]
1760    fn delete_key_package_removes_vc_record() {
1761        let provider = OpenMlsRustCrypto::default();
1762        let leaf_index = LeafNodeIndex::new(0);
1763        let epoch_id = register_epoch_state(&provider, leaf_index);
1764        let kp_ref = KeyPackageRef::from_slice(b"kp-ref");
1765        let upload = KeyPackageUpload {
1766            epoch_id,
1767            leaf_index,
1768            generation: 0,
1769            key_package_info: vec![KeyPackageInfo {
1770                key_package_ref: kp_ref.clone(),
1771                cipher_suite: CIPHERSUITE,
1772                key_package_index: 0,
1773            }],
1774        };
1775        process_vc_key_package_upload(&provider, &upload).expect("process upload");
1776
1777        let present: Option<RetainedKeyPackageMaterial> = <MemoryStorage as StorageProvider<
1778            CURRENT_VERSION,
1779        >>::retained_key_package_material(
1780            provider.storage(), &kp_ref
1781        )
1782        .expect("read material");
1783        assert!(present.is_some());
1784
1785        <MemoryStorage as StorageProvider<CURRENT_VERSION>>::delete_key_package(
1786            provider.storage(),
1787            &kp_ref,
1788        )
1789        .expect("delete key package");
1790
1791        let after: Option<RetainedKeyPackageMaterial> = <MemoryStorage as StorageProvider<
1792            CURRENT_VERSION,
1793        >>::retained_key_package_material(
1794            provider.storage(), &kp_ref
1795        )
1796        .expect("read material after delete");
1797        assert!(after.is_none());
1798    }
1799
1800    fn setup_key_and_epoch_id(provider: &OpenMlsRustCrypto) -> (EpochEncryptionKey, EpochId) {
1801        let emulator = EmulatorEpochSecret::new(
1802            &provider
1803                .rand()
1804                .random_vec(CIPHERSUITE.hash_length())
1805                .expect("randomness"),
1806        );
1807        let key = emulator
1808            .derive_epoch_encryption_key(provider.crypto(), CIPHERSUITE)
1809            .expect("derive ek");
1810        let epoch_id = emulator
1811            .derive_epoch_id(provider.crypto(), CIPHERSUITE)
1812            .expect("derive epoch id");
1813        (key, epoch_id)
1814    }
1815
1816    /// Round-trip both `DerivationInfoTbe` variants through `encrypt` and
1817    /// `decrypt`. Catches any disagreement between the two methods on the
1818    /// derived key/nonce, the AAD, or the tagless TLS layout of the
1819    /// plaintext, and confirms each variant decodes only under its own
1820    /// operation type.
1821    #[test]
1822    fn derivation_info_tbe_roundtrip() {
1823        let provider = OpenMlsRustCrypto::default();
1824        let (key, epoch_id) = setup_key_and_epoch_id(&provider);
1825        let leaf_encryption_key = provider.rand().random_vec(32).expect("randomness");
1826
1827        let key_package_tbe = DerivationInfoTbe::KeyPackage {
1828            leaf_index: LeafNodeIndex::new(7),
1829            generation: 3,
1830            key_package_index: 5,
1831        };
1832        let leaf_node_tbe = DerivationInfoTbe::LeafNode {
1833            leaf_index: LeafNodeIndex::new(7),
1834            generation: 3,
1835            external_init_secret: None,
1836        };
1837        let external_commit_tbe = DerivationInfoTbe::LeafNode {
1838            leaf_index: LeafNodeIndex::new(7),
1839            generation: 3,
1840            external_init_secret: Some(ExternalInitSecret::from_slice(b"external init secret")),
1841        };
1842
1843        // The key_package form carries the trailing key_package_index (u32),
1844        // while the leaf_node (commit) form carries an absent
1845        // optional<ExternalInitSecret> (one presence octet).
1846        let key_package_bytes = key_package_tbe
1847            .tls_serialize_detached()
1848            .expect("serialize key package tbe");
1849        let leaf_node_bytes = leaf_node_tbe
1850            .tls_serialize_detached()
1851            .expect("serialize leaf node tbe");
1852        assert_eq!(key_package_bytes.len(), leaf_node_bytes.len() + 3);
1853
1854        for (original, operation_type) in [
1855            (key_package_tbe, VirtualClientOperationType::KeyPackage),
1856            (leaf_node_tbe, VirtualClientOperationType::LeafNode),
1857            (external_commit_tbe, VirtualClientOperationType::LeafNode),
1858        ] {
1859            let derivation_info = DerivationInfo::encrypt(
1860                provider.crypto(),
1861                CIPHERSUITE,
1862                &key,
1863                epoch_id.clone(),
1864                &leaf_encryption_key,
1865                &original,
1866            )
1867            .expect("encrypt");
1868            assert_eq!(derivation_info.epoch_id(), &epoch_id);
1869            let decrypted = derivation_info
1870                .decrypt(
1871                    provider.crypto(),
1872                    CIPHERSUITE,
1873                    &key,
1874                    &leaf_encryption_key,
1875                    operation_type,
1876                )
1877                .expect("decrypt");
1878            assert_eq!(original, decrypted);
1879        }
1880    }
1881
1882    /// Pin the serialized `DerivationInfoTBE` layout to the spec's select,
1883    /// byte for byte: `uint32 leaf_index`, `uint32 generation`, then the
1884    /// `key_package_index` (key_package case) or the
1885    /// `optional<ExternalInitSecret>` (commit case) with nothing trailing.
1886    /// Catches conventions drift that the roundtrip test cannot see.
1887    #[test]
1888    fn derivation_info_tbe_wire_format_matches_spec() {
1889        let absent = DerivationInfoTbe::LeafNode {
1890            leaf_index: LeafNodeIndex::new(7),
1891            generation: 3,
1892            external_init_secret: None,
1893        }
1894        .tls_serialize_detached()
1895        .expect("serialize");
1896        assert_eq!(
1897            absent,
1898            [0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x03, 0x00]
1899        );
1900
1901        let present = DerivationInfoTbe::LeafNode {
1902            leaf_index: LeafNodeIndex::new(7),
1903            generation: 3,
1904            external_init_secret: Some(ExternalInitSecret::from_slice(b"init")),
1905        }
1906        .tls_serialize_detached()
1907        .expect("serialize");
1908        assert_eq!(
1909            present,
1910            [0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x03, 0x01, 0x04, b'i', b'n', b'i', b't']
1911        );
1912
1913        let key_package = DerivationInfoTbe::KeyPackage {
1914            leaf_index: LeafNodeIndex::new(7),
1915            generation: 3,
1916            key_package_index: 5,
1917        }
1918        .tls_serialize_detached()
1919        .expect("serialize");
1920        assert_eq!(
1921            key_package,
1922            [0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x05]
1923        );
1924    }
1925
1926    /// The TBE plaintext must be consumed exactly. A trailing octet, which is
1927    /// what a peer implementing the superseded draft revision with its
1928    /// trailing `optional<GroupCreationSecret>` would produce, is rejected
1929    /// for both variants.
1930    #[test]
1931    fn derivation_info_tbe_rejects_trailing_data() {
1932        let variants = [
1933            (
1934                DerivationInfoTbe::LeafNode {
1935                    leaf_index: LeafNodeIndex::new(7),
1936                    generation: 3,
1937                    external_init_secret: None,
1938                },
1939                VirtualClientOperationType::LeafNode,
1940            ),
1941            (
1942                DerivationInfoTbe::KeyPackage {
1943                    leaf_index: LeafNodeIndex::new(7),
1944                    generation: 3,
1945                    key_package_index: 5,
1946                },
1947                VirtualClientOperationType::KeyPackage,
1948            ),
1949        ];
1950        for (tbe, operation_type) in variants {
1951            let mut bytes = tbe.tls_serialize_detached().expect("serialize");
1952            bytes.push(0x00);
1953            let result = DerivationInfoTbe::deserialize_for_operation(&bytes, operation_type);
1954            assert_eq!(result, Err(VirtualClientsError::DerivationInfoMalformed));
1955        }
1956    }
1957
1958    /// Debug output of the TBE must not leak the carried init secret.
1959    #[test]
1960    fn external_init_secret_debug_is_redacted() {
1961        let tbe = DerivationInfoTbe::LeafNode {
1962            leaf_index: LeafNodeIndex::new(7),
1963            generation: 3,
1964            external_init_secret: Some(ExternalInitSecret::from_slice(b"very secret bytes")),
1965        };
1966        let debug = format!("{tbe:?}");
1967        assert!(debug.contains("<redacted>"));
1968        assert!(!debug.contains("secret bytes"));
1969        assert!(!debug.to_lowercase().contains("76657279"));
1970    }
1971
1972    /// Decryption must fail when the leaf encryption key used as the
1973    /// key/nonce derivation context does not match the one used for
1974    /// encryption. This is what binds the derivation info to the leaf
1975    /// that carries it.
1976    #[test]
1977    fn decryption_fails_with_wrong_leaf_encryption_key() {
1978        let provider = OpenMlsRustCrypto::default();
1979        let (key, epoch_id) = setup_key_and_epoch_id(&provider);
1980        let leaf_encryption_key = provider.rand().random_vec(32).expect("randomness");
1981        let tbe = DerivationInfoTbe::LeafNode {
1982            leaf_index: LeafNodeIndex::new(1),
1983            generation: 0,
1984            external_init_secret: None,
1985        };
1986        let derivation_info = DerivationInfo::encrypt(
1987            provider.crypto(),
1988            CIPHERSUITE,
1989            &key,
1990            epoch_id,
1991            &leaf_encryption_key,
1992            &tbe,
1993        )
1994        .expect("encrypt");
1995        let other_leaf_encryption_key = provider.rand().random_vec(32).expect("randomness");
1996        let err = derivation_info
1997            .decrypt(
1998                provider.crypto(),
1999                CIPHERSUITE,
2000                &key,
2001                &other_leaf_encryption_key,
2002                VirtualClientOperationType::LeafNode,
2003            )
2004            .expect_err("decryption with the wrong context must fail");
2005        assert_eq!(err, VirtualClientsError::DerivationInfoDecryptionFailed);
2006    }
2007
2008    /// The per-KeyPackage seed secret is deterministic for a given index,
2009    /// distinct across indices, and the init and encryption keys derived from
2010    /// one seed are separated from each other.
2011    #[test]
2012    fn key_package_seed_derivation_is_indexed_and_label_separated() {
2013        let provider = OpenMlsRustCrypto::default();
2014        let operation_secret = OperationSecret::from(Secret::from_slice(
2015            &provider
2016                .rand()
2017                .random_vec(CIPHERSUITE.hash_length())
2018                .expect("randomness"),
2019        ));
2020
2021        let seed_zero = operation_secret
2022            .derive_key_package_seed_secret(provider.crypto(), CIPHERSUITE, 0)
2023            .expect("derive seed 0");
2024        let seed_zero_again = operation_secret
2025            .derive_key_package_seed_secret(provider.crypto(), CIPHERSUITE, 0)
2026            .expect("derive seed 0 again");
2027        let seed_one = operation_secret
2028            .derive_key_package_seed_secret(provider.crypto(), CIPHERSUITE, 1)
2029            .expect("derive seed 1");
2030
2031        let init_zero = seed_zero
2032            .derive_init_key_secret(provider.crypto(), CIPHERSUITE)
2033            .expect("derive init key 0")
2034            .generate_init_key_pair(provider.crypto(), CIPHERSUITE)
2035            .expect("generate init pair 0");
2036        let init_zero_again = seed_zero_again
2037            .derive_init_key_secret(provider.crypto(), CIPHERSUITE)
2038            .expect("derive init key 0 again")
2039            .generate_init_key_pair(provider.crypto(), CIPHERSUITE)
2040            .expect("generate init pair 0 again");
2041        let init_one = seed_one
2042            .derive_init_key_secret(provider.crypto(), CIPHERSUITE)
2043            .expect("derive init key 1")
2044            .generate_init_key_pair(provider.crypto(), CIPHERSUITE)
2045            .expect("generate init pair 1");
2046
2047        // Same index derives deterministically.
2048        assert_eq!(init_zero.public, init_zero_again.public);
2049        // Different indices derive distinct seeds, hence distinct init keys.
2050        assert_ne!(init_zero.public, init_one.public);
2051
2052        // Init and encryption keys from one seed are label-separated.
2053        let encryption_zero = seed_zero
2054            .derive_encryption_key_secret(provider.crypto(), CIPHERSUITE)
2055            .expect("derive encryption key 0")
2056            .generate_encryption_key_pair(provider.crypto(), CIPHERSUITE)
2057            .expect("generate encryption pair 0");
2058        assert_ne!(
2059            init_zero.public.as_slice(),
2060            encryption_zero.public_key().as_slice()
2061        );
2062    }
2063
2064    /// The per-KeyPackage seed is imported into the target ciphersuite: the
2065    /// same operation secret and index yield different seeds for different
2066    /// target ciphersuites, because the target ciphersuite is bound into the
2067    /// `KeyPackageSeedContext` and the import runs under the target's KDF.
2068    #[test]
2069    fn key_package_seed_binds_target_ciphersuite() {
2070        let provider = OpenMlsRustCrypto::default();
2071        let operation_secret = OperationSecret::from(Secret::from_slice(
2072            &provider
2073                .rand()
2074                .random_vec(CIPHERSUITE.hash_length())
2075                .expect("randomness"),
2076        ));
2077        // Same KDF hash (SHA-256) as `CIPHERSUITE`, so the two seeds have
2078        // equal length and differ only through the ciphersuite binding.
2079        let other_ciphersuite = Ciphersuite::MLS_128_DHKEMX25519_CHACHA20POLY1305_SHA256_Ed25519;
2080
2081        let seed = operation_secret
2082            .derive_key_package_seed_secret(provider.crypto(), CIPHERSUITE, 0)
2083            .expect("derive seed");
2084        let seed_other_suite = operation_secret
2085            .derive_key_package_seed_secret(provider.crypto(), other_ciphersuite, 0)
2086            .expect("derive seed under other target ciphersuite");
2087
2088        assert_ne!(seed.0.as_slice(), seed_other_suite.0.as_slice());
2089    }
2090
2091    /// The `target_operation_secret` of a `leaf_node` operation is
2092    /// deterministic and binds both the target ciphersuite and the
2093    /// higher-level group's id; the encryption and path-generation secrets
2094    /// derived from it are label-separated.
2095    #[test]
2096    fn target_operation_secret_binds_ciphersuite_and_group_id() {
2097        let provider = OpenMlsRustCrypto::default();
2098        let operation_secret = OperationSecret::from(Secret::from_slice(
2099            &provider
2100                .rand()
2101                .random_vec(CIPHERSUITE.hash_length())
2102                .expect("randomness"),
2103        ));
2104        let group_id = GroupId::from_slice(b"group-a");
2105        let other_ciphersuite = Ciphersuite::MLS_128_DHKEMX25519_CHACHA20POLY1305_SHA256_Ed25519;
2106
2107        let target = operation_secret
2108            .derive_target_operation_secret(provider.crypto(), CIPHERSUITE, &group_id)
2109            .expect("derive target operation secret");
2110        let target_again = operation_secret
2111            .derive_target_operation_secret(provider.crypto(), CIPHERSUITE, &group_id)
2112            .expect("derive target operation secret again");
2113        let target_other_group = operation_secret
2114            .derive_target_operation_secret(
2115                provider.crypto(),
2116                CIPHERSUITE,
2117                &GroupId::from_slice(b"group-b"),
2118            )
2119            .expect("derive target operation secret for other group");
2120        let target_other_suite = operation_secret
2121            .derive_target_operation_secret(provider.crypto(), other_ciphersuite, &group_id)
2122            .expect("derive target operation secret under other target ciphersuite");
2123
2124        // Same inputs derive deterministically.
2125        assert_eq!(target.0.as_slice(), target_again.0.as_slice());
2126        // A different group id or a different target ciphersuite derives a
2127        // distinct secret.
2128        assert_ne!(target.0.as_slice(), target_other_group.0.as_slice());
2129        assert_ne!(target.0.as_slice(), target_other_suite.0.as_slice());
2130
2131        // Encryption and path-generation secrets from one target operation
2132        // secret are label-separated.
2133        let encryption_key_secret = target
2134            .derive_encryption_key_secret(provider.crypto(), CIPHERSUITE)
2135            .expect("derive encryption key secret");
2136        let path_generation_secret = target
2137            .derive_path_generation_secret(provider.crypto(), CIPHERSUITE)
2138            .expect("derive path generation secret");
2139        assert_ne!(
2140            encryption_key_secret.0.as_slice(),
2141            path_generation_secret.0.as_slice()
2142        );
2143    }
2144
2145    /// The group-creation epoch secret is deterministic for a given seed,
2146    /// distinct across seeds, and label-separated from the encryption key
2147    /// secret derived from the same seed.
2148    #[test]
2149    fn group_creation_secret_derivation_is_deterministic_and_label_separated() {
2150        let provider = OpenMlsRustCrypto::default();
2151        let operation_secret = OperationSecret::from(Secret::from_slice(
2152            &provider
2153                .rand()
2154                .random_vec(CIPHERSUITE.hash_length())
2155                .expect("randomness"),
2156        ));
2157
2158        let seed_zero = operation_secret
2159            .derive_key_package_seed_secret(provider.crypto(), CIPHERSUITE, 0)
2160            .expect("derive seed 0");
2161        let seed_one = operation_secret
2162            .derive_key_package_seed_secret(provider.crypto(), CIPHERSUITE, 1)
2163            .expect("derive seed 1");
2164
2165        let epoch_secret_zero = seed_zero
2166            .derive_group_creation_secret(provider.crypto(), CIPHERSUITE)
2167            .expect("derive group creation secret 0");
2168        let epoch_secret_zero_again = seed_zero
2169            .derive_group_creation_secret(provider.crypto(), CIPHERSUITE)
2170            .expect("derive group creation secret 0 again");
2171        let epoch_secret_one = seed_one
2172            .derive_group_creation_secret(provider.crypto(), CIPHERSUITE)
2173            .expect("derive group creation secret 1");
2174
2175        // Same seed derives deterministically.
2176        assert_eq!(
2177            epoch_secret_zero.as_slice(),
2178            epoch_secret_zero_again.as_slice()
2179        );
2180        // Different seeds derive distinct epoch secrets.
2181        assert_ne!(epoch_secret_zero.as_slice(), epoch_secret_one.as_slice());
2182
2183        // The epoch secret is label-separated from the encryption key secret
2184        // derived from the same seed.
2185        let encryption_key_secret = seed_zero
2186            .derive_encryption_key_secret(provider.crypto(), CIPHERSUITE)
2187            .expect("derive encryption key 0");
2188        assert_ne!(
2189            epoch_secret_zero.as_slice(),
2190            encryption_key_secret.0.as_slice()
2191        );
2192    }
2193
2194    /// A repeated `key_package_index` is rejected with
2195    /// `DuplicateKeyPackageIndex` carrying the offending index.
2196    #[test]
2197    fn validate_rejects_duplicate_index() {
2198        let infos = vec![
2199            KeyPackageInfo {
2200                key_package_ref: KeyPackageRef::from_slice(b"kp-ref-a"),
2201                cipher_suite: CIPHERSUITE,
2202                key_package_index: 2,
2203            },
2204            KeyPackageInfo {
2205                key_package_ref: KeyPackageRef::from_slice(b"kp-ref-b"),
2206                cipher_suite: CIPHERSUITE,
2207                key_package_index: 2,
2208            },
2209        ];
2210        let err = validate_key_package_infos(&infos).expect_err("duplicate index must be rejected");
2211        assert_eq!(err, VirtualClientsError::DuplicateKeyPackageIndex(2));
2212    }
2213
2214    /// A repeated `KeyPackageRef` is rejected with `DuplicateKeyPackageRef`.
2215    #[test]
2216    fn validate_rejects_duplicate_ref() {
2217        let infos = vec![
2218            KeyPackageInfo {
2219                key_package_ref: KeyPackageRef::from_slice(b"kp-ref-a"),
2220                cipher_suite: CIPHERSUITE,
2221                key_package_index: 0,
2222            },
2223            KeyPackageInfo {
2224                key_package_ref: KeyPackageRef::from_slice(b"kp-ref-a"),
2225                cipher_suite: CIPHERSUITE,
2226                key_package_index: 1,
2227            },
2228        ];
2229        let err = validate_key_package_infos(&infos).expect_err("duplicate ref must be rejected");
2230        assert_eq!(err, VirtualClientsError::DuplicateKeyPackageRef);
2231    }
2232
2233    /// A batch with distinct indices and references passes validation.
2234    #[test]
2235    fn validate_accepts_distinct_infos() {
2236        let infos = vec![
2237            KeyPackageInfo {
2238                key_package_ref: KeyPackageRef::from_slice(b"kp-ref-a"),
2239                cipher_suite: CIPHERSUITE,
2240                key_package_index: 0,
2241            },
2242            KeyPackageInfo {
2243                key_package_ref: KeyPackageRef::from_slice(b"kp-ref-b"),
2244                cipher_suite: CIPHERSUITE,
2245                key_package_index: 1,
2246            },
2247        ];
2248        validate_key_package_infos(&infos).expect("distinct infos must pass");
2249    }
2250
2251    /// A malformed upload is rejected before the batch generation is consumed,
2252    /// so a later valid upload reusing the same generation still succeeds and
2253    /// stores its retained material.
2254    #[test]
2255    fn process_upload_rejects_malformed_without_consuming_generation() {
2256        let provider = OpenMlsRustCrypto::default();
2257        let leaf_index = LeafNodeIndex::new(0);
2258        let epoch_id = register_epoch_state(&provider, leaf_index);
2259        let ref_a = KeyPackageRef::from_slice(b"kp-ref-a");
2260        let ref_b = KeyPackageRef::from_slice(b"kp-ref-b");
2261
2262        let malformed = KeyPackageUpload {
2263            epoch_id: epoch_id.clone(),
2264            leaf_index,
2265            generation: 0,
2266            key_package_info: vec![
2267                KeyPackageInfo {
2268                    key_package_ref: ref_a.clone(),
2269                    cipher_suite: CIPHERSUITE,
2270                    key_package_index: 0,
2271                },
2272                KeyPackageInfo {
2273                    key_package_ref: ref_b.clone(),
2274                    cipher_suite: CIPHERSUITE,
2275                    key_package_index: 0,
2276                },
2277            ],
2278        };
2279        let err = process_vc_key_package_upload(&provider, &malformed)
2280            .expect_err("malformed upload must be rejected");
2281        assert_eq!(err, VirtualClientsError::DuplicateKeyPackageIndex(0));
2282
2283        let valid = KeyPackageUpload {
2284            epoch_id: epoch_id.clone(),
2285            leaf_index,
2286            generation: 0,
2287            key_package_info: vec![
2288                KeyPackageInfo {
2289                    key_package_ref: ref_a.clone(),
2290                    cipher_suite: CIPHERSUITE,
2291                    key_package_index: 0,
2292                },
2293                KeyPackageInfo {
2294                    key_package_ref: ref_b.clone(),
2295                    cipher_suite: CIPHERSUITE,
2296                    key_package_index: 1,
2297                },
2298            ],
2299        };
2300        process_vc_key_package_upload(&provider, &valid)
2301            .expect("valid upload reusing the same generation must succeed");
2302
2303        let material_a: RetainedKeyPackageMaterial = <MemoryStorage as StorageProvider<
2304            CURRENT_VERSION,
2305        >>::retained_key_package_material(
2306            provider.storage(), &ref_a
2307        )
2308        .expect("read material a")
2309        .expect("material a present");
2310        assert_eq!(material_a.epoch_id, epoch_id);
2311        assert_eq!(material_a.generation, 0);
2312        assert_eq!(material_a.key_package_index, 0);
2313
2314        let material_b: RetainedKeyPackageMaterial = <MemoryStorage as StorageProvider<
2315            CURRENT_VERSION,
2316        >>::retained_key_package_material(
2317            provider.storage(), &ref_b
2318        )
2319        .expect("read material b")
2320        .expect("material b present");
2321        assert_eq!(material_b.key_package_index, 1);
2322    }
2323
2324    /// Build an `EmulationEpochState` from raw emulator-epoch-secret bytes, so
2325    /// two siblings sharing the same bytes can be compared.
2326    fn state_from_secret_bytes(
2327        provider: &OpenMlsRustCrypto,
2328        secret_bytes: &[u8],
2329        leaf_index: LeafNodeIndex,
2330    ) -> EmulationEpochState {
2331        let emulator = EmulatorEpochSecret::new(secret_bytes);
2332        let epoch_encryption_key = emulator
2333            .derive_epoch_encryption_key(provider.crypto(), CIPHERSUITE)
2334            .expect("derive epoch encryption key");
2335        let reuse_guard_secret = emulator
2336            .derive_reuse_guard_secret(provider.crypto(), CIPHERSUITE)
2337            .expect("derive reuse guard secret");
2338        let generation_id_secret = emulator
2339            .derive_generation_id_secret(provider.crypto(), CIPHERSUITE)
2340            .expect("derive generation id secret");
2341        EmulationEpochState::new(
2342            leaf_index,
2343            epoch_encryption_key,
2344            reuse_guard_secret,
2345            generation_id_secret,
2346            TreeSize::new(2),
2347            CIPHERSUITE,
2348        )
2349    }
2350
2351    /// The generation ID is deterministic for fixed inputs, changes when any
2352    /// `PrivateMessageContext` field changes, and two siblings that share the
2353    /// same emulator epoch secret derive the same value (so a DS can compare
2354    /// them for equality across siblings).
2355    #[test]
2356    fn generation_id_is_deterministic_and_context_sensitive() {
2357        let provider = OpenMlsRustCrypto::default();
2358        let secret_bytes = provider
2359            .rand()
2360            .random_vec(CIPHERSUITE.hash_length())
2361            .expect("randomness");
2362        let state = state_from_secret_bytes(&provider, &secret_bytes, LeafNodeIndex::new(0));
2363
2364        let group_id = GroupId::from_slice(b"higher-level-group");
2365        let epoch = GroupEpoch::from(7);
2366        let derive = |group_id: &GroupId, epoch, generation, ratchet_type| {
2367            state
2368                .derive_generation_id(provider.crypto(), group_id, epoch, generation, ratchet_type)
2369                .expect("derive generation id")
2370        };
2371
2372        let base = derive(&group_id, epoch, 3, RatchetType::Application);
2373        // The generation ID is `Kdf.Nh` bytes long.
2374        assert_eq!(base.as_slice().len(), CIPHERSUITE.hash_length());
2375        // Deterministic for fixed inputs.
2376        assert_eq!(base, derive(&group_id, epoch, 3, RatchetType::Application));
2377        // Sensitive to the generation, the epoch, the group id, and the
2378        // ratchet type.
2379        assert_ne!(base, derive(&group_id, epoch, 4, RatchetType::Application));
2380        assert_ne!(
2381            base,
2382            derive(&group_id, GroupEpoch::from(8), 3, RatchetType::Application)
2383        );
2384        assert_ne!(
2385            base,
2386            derive(
2387                &GroupId::from_slice(b"other-group"),
2388                epoch,
2389                3,
2390                RatchetType::Application
2391            )
2392        );
2393        assert_ne!(base, derive(&group_id, epoch, 3, RatchetType::Handshake));
2394
2395        // A sibling sharing the same emulator epoch secret derives the same
2396        // generation ID, even from a different leaf index: the leaf index is
2397        // not part of the PrivateMessageContext.
2398        let sibling = state_from_secret_bytes(&provider, &secret_bytes, LeafNodeIndex::new(5));
2399        let sibling_id = sibling
2400            .derive_generation_id(
2401                provider.crypto(),
2402                &group_id,
2403                epoch,
2404                3,
2405                RatchetType::Application,
2406            )
2407            .expect("sibling derive generation id");
2408        assert_eq!(base, sibling_id);
2409    }
2410}