Skip to main content

openmls/components/
vc_derivation_info.rs

1use std::collections::{BTreeSet, VecDeque};
2#[cfg(not(target_arch = "wasm32"))]
3use std::time::SystemTime;
4
5use openmls_traits::{
6    crypto::OpenMlsCrypto,
7    types::{Ciphersuite, CryptoError},
8    OpenMlsProvider,
9};
10use serde::{Deserialize, Serialize};
11use thiserror::Error;
12use tls_codec::{
13    DeserializeBytes, SecretVLByteVec, Serialize as _, Size as _, TlsDeserializeBytes,
14    TlsSerialize, TlsSize, VLByteSlice, VLByteVec,
15};
16#[cfg(target_arch = "wasm32")]
17use web_time::SystemTime;
18
19use crate::{
20    binary_tree::{array_representation::TreeSize, LeafNodeIndex},
21    ciphersuite::{hash_ref::KeyPackageRef, Secret},
22    components::vc_operation_tree::OperationSecretTree,
23    group::{
24        mls_group::errors::RegisterVcDerivationEpochError, GroupEpoch, GroupId,
25        VcDerivationEpochRetentionPolicy,
26    },
27    key_packages::InitKey,
28    messages::PathSecret,
29    schedule::application_export_tree::{ApplicationExportTree, ApplicationExportTreeError},
30    treesync::node::encryption_keys::EncryptionKeyPair,
31};
32
33/// Component ID under which the virtual-clients derivation info is carried in
34/// the leaf node's `app_data_dictionary` extension.
35///
36/// `0x667A` is the temporary, random value until the draft is further along the
37/// publication process.
38pub const VC_COMPONENT_ID: u16 = 0x667A;
39
40// Operation-secret child labels. Each child is derived from the per-operation
41// secret produced by the per-epoch operation secret tree. `Encryption Key`
42// and `Path Generation` cover the `leaf_node` commit path, and `Init Key`
43// covers the `key_package` operation path. The spec also defines a
44// `Signature Key` child, which together with the operation paths that consume
45// it is deferred to a follow-up PR.
46const ENCRYPTION_KEY_LABEL: &str = "Encryption Key";
47const PATH_GENERATION_LABEL: &str = "Path Generation";
48const INIT_KEY_LABEL: &str = "Init Key";
49/// `ImportSecret` label for the per-KeyPackage seed secret derived from a
50/// `key_package` operation secret (mls-virtual-clients draft, batch KeyPackage
51/// derivation). One operation secret covers a batch of KeyPackages, and each
52/// KeyPackage's seed is imported from it using the KeyPackage's ciphersuite
53/// and index as the context.
54const KEY_PACKAGE_SEED_LABEL: &str = "vc key package seed";
55/// `ImportSecret` label for `target_operation_secret` of a `leaf_node`:
56/// imports operation secret into the higher-level group's ciphersuite.
57const TARGET_OPERATION_LABEL: &str = "vc target operation";
58/// `DeriveSecret` label for the epoch-0 `epoch_secret` a group creator derives
59/// from its KeyPackage seed secret (mls-virtual-clients draft, group creation).
60const GROUP_CREATION_LABEL: &str = "Group Creation";
61
62/// `ExpandWithLabel` label for the [`DerivationInfoTbe`] AEAD key derived
63/// from the per-epoch [`EpochEncryptionKey`].
64const DERIVATION_INFO_KEY_LABEL: &str = "key";
65/// `ExpandWithLabel` label for the [`DerivationInfoTbe`] AEAD nonce derived
66/// from the per-epoch [`EpochEncryptionKey`].
67const DERIVATION_INFO_NONCE_LABEL: &str = "nonce";
68
69const EPOCH_ID_LABEL: &str = "Epoch ID";
70const EPOCH_ENCRYPTION_KEY_LABEL: &str = "Encryption Key";
71const EPOCH_BASE_SECRET_LABEL: &str = "Base Secret";
72/// `DeriveSecret` label for [`ReuseGuardSecret`].
73const REUSE_GUARD_LABEL: &str = "Reuse Guard";
74/// `DeriveSecret` label for [`GenerationIdSecret`].
75const GENERATION_ID_LABEL: &str = "Generation ID Secret";
76/// `ExpandWithLabel` label for a [`GenerationId`] derived from a
77/// [`GenerationIdSecret`] over a serialized [`PrivateMessageContext`]
78/// (mls-virtual-clients draft, generation-ID section).
79const GENERATION_ID_EXPAND_LABEL: &str = "generation id";
80/// `ExpandWithLabel` label for the 16-byte FF1 PRP key derived from a
81/// [`ReuseGuardSecret`] (mls-virtual-clients draft, Reuse Guard section).
82const REUSE_GUARD_PRP_KEY_LABEL: &str = "reuse guard";
83/// FF1 PRP key length in bytes (AES-128).
84const PRP_KEY_LEN: usize = 16;
85
86/// Errors that can occur while processing virtual-clients derivation info.
87#[derive(Error, Debug, PartialEq, Clone)]
88pub enum VirtualClientsError {
89    /// The derivation-info bytes failed to deserialize.
90    #[error("Failed to deserialize derivation info.")]
91    DerivationInfoMalformed,
92    /// AEAD decryption of the encrypted derivation info failed (wrong key,
93    /// tampered ciphertext, or mismatched AAD).
94    #[error("Failed to decrypt derivation info.")]
95    DerivationInfoDecryptionFailed,
96    /// No virtual-clients operation secret tree was registered for this
97    /// epoch.
98    #[error("No virtual-clients operation secret tree for this epoch.")]
99    MissingOperationTree,
100    /// No virtual-clients `VcDerivationEpochState` was registered for this
101    /// epoch, or it has been deleted.
102    #[error("No virtual-clients derivation-epoch state for this epoch.")]
103    MissingDerivationEpochState,
104    /// No derivation epoch is registered for the group a new virtual-client
105    /// operation was resolved against. The operation requires that group to be
106    /// an emulation group with a registered derivation epoch.
107    #[error("No derivation epoch is registered for the group.")]
108    NoDerivationEpoch,
109    /// Loading or storing virtual-clients state via the storage provider
110    /// failed.
111    #[error("Virtual-clients storage error")]
112    StorageError,
113    /// The leaf encryption key in the path does not match the key derived
114    /// from the path secret.
115    #[error("Leaf encryption key from path does not match the derived key.")]
116    EncryptionKeyMismatch,
117    /// A cryptographic operation failed during virtual-clients processing.
118    #[error("Cryptographic operation failed.")]
119    CryptoError(#[from] CryptoError),
120    /// Hash function produced output of unexpected length.
121    #[error(
122        "Hash function produced output of length {actual_length}, expected {expected_length}."
123    )]
124    HashOutputLengthMismatch {
125        /// The number of bytes in the hash output.
126        actual_length: usize,
127        /// The required number of bytes in the hash output.
128        expected_length: usize,
129    },
130    /// TLS encoding/decoding of a virtual-clients structure failed. Covers
131    /// both serialization on the sender side and deserialization of the
132    /// decrypted `DerivationInfoTbe` on the receiver side.
133    #[error("TLS codec error: {0}")]
134    Tls(#[from] tls_codec::Error),
135    /// The leaf carrying (or about to carry) a VC derivation-info entry
136    /// does not declare `AppDataDictionary` in its capabilities.
137    #[error("Leaf does not declare AppDataDictionary support in its capabilities.")]
138    AppDataDictionaryNotSupported,
139    /// The leaf's `AppDataDictionary` extension is missing the
140    /// `AppComponents` entry, or that entry does not list
141    /// [`VC_COMPONENT_ID`].
142    #[error("Leaf's AppComponents entry does not list the virtual-clients component id.")]
143    VcComponentNotListed,
144    /// The requested leaf index lies outside the operation secret tree.
145    #[error("Leaf index is outside the operation secret tree.")]
146    IndexOutOfBounds,
147    /// The coordinates of a sibling's operation name the calling client's own
148    /// leaf index.
149    #[error("The operation coordinates name the caller's own leaf index.")]
150    OwnLeafIndex,
151    /// The operation secret for the requested generation was already derived
152    /// and deleted for forward secrecy.
153    #[error("The operation secret for this generation was already consumed.")]
154    OperationGenerationConsumed,
155    /// The requested operation generation lies too far beyond the current
156    /// ratchet head (see `MAXIMUM_FORWARD_DISTANCE` in the operation secret
157    /// tree).
158    #[error("The requested operation generation is too far beyond the ratchet head.")]
159    OperationGenerationTooDistant,
160    /// An operation ratchet has reached the maximum generation.
161    #[error("Operation ratchet generation has reached `u32::MAX`.")]
162    OperationRatchetTooLong,
163    /// An unrecoverable error has occurred due to a bug in the
164    /// implementation.
165    #[error("An unrecoverable error has occurred due to a bug in the implementation.")]
166    LibraryError,
167    /// The `KeyPackageUpload` lists the same `key_package_index` more than
168    /// once. Each batch index must appear at most once.
169    #[error("KeyPackageUpload contains a duplicate key_package_index: {0}.")]
170    DuplicateKeyPackageIndex(u32),
171    /// The `KeyPackageUpload` lists the same [`KeyPackageRef`] more than once.
172    /// Each KeyPackage reference must appear at most once.
173    #[error("KeyPackageUpload contains a duplicate KeyPackageRef.")]
174    DuplicateKeyPackageRef,
175}
176
177/// Per-derivation-epoch root secret. Sourced internally from the emulation
178/// group's `safe_export_secret(VC_COMPONENT_ID)` when a derivation epoch is
179/// registered.
180#[derive(Debug, Serialize, Deserialize)]
181pub(crate) struct EmulatorEpochSecret(Secret);
182
183impl EmulatorEpochSecret {
184    /// Construct an `EmulatorEpochSecret` from raw bytes. Bytes are
185    /// expected to be the output of the emulation group's
186    /// `safe_export_secret(VC_COMPONENT_ID)`.
187    pub(crate) fn new(bytes: &[u8]) -> Self {
188        Self(Secret::from_slice(bytes))
189    }
190
191    pub(crate) fn derive_epoch_id(
192        &self,
193        crypto: &impl OpenMlsCrypto,
194        ciphersuite: Ciphersuite,
195    ) -> Result<EpochId, VirtualClientsError> {
196        let secret = self.0.derive_secret(crypto, ciphersuite, EPOCH_ID_LABEL)?;
197        Ok(EpochId(secret.as_slice().to_vec().into()))
198    }
199
200    /// Derive the per-epoch [`EpochEncryptionKey`]. The key is a KDF
201    /// secret (the per-leaf AEAD key and nonce are expanded from it), so
202    /// it is derived at the KDF's hash length.
203    pub(crate) fn derive_epoch_encryption_key(
204        &self,
205        crypto: &impl OpenMlsCrypto,
206        ciphersuite: Ciphersuite,
207    ) -> Result<EpochEncryptionKey, VirtualClientsError> {
208        let secret = self
209            .0
210            .derive_secret(crypto, ciphersuite, EPOCH_ENCRYPTION_KEY_LABEL)?;
211        Ok(EpochEncryptionKey(secret))
212    }
213
214    pub(crate) fn derive_epoch_base_secret(
215        &self,
216        crypto: &impl OpenMlsCrypto,
217        ciphersuite: Ciphersuite,
218    ) -> Result<Secret, VirtualClientsError> {
219        Ok(self
220            .0
221            .derive_secret(crypto, ciphersuite, EPOCH_BASE_SECRET_LABEL)?)
222    }
223
224    /// Derive the per-derivation-epoch [`ReuseGuardSecret`].
225    pub(crate) fn derive_reuse_guard_secret(
226        &self,
227        crypto: &impl OpenMlsCrypto,
228        ciphersuite: Ciphersuite,
229    ) -> Result<ReuseGuardSecret, VirtualClientsError> {
230        let secret = self
231            .0
232            .derive_secret(crypto, ciphersuite, REUSE_GUARD_LABEL)?;
233        Ok(ReuseGuardSecret(secret))
234    }
235
236    /// Derive the per-derivation-epoch [`GenerationIdSecret`].
237    pub(crate) fn derive_generation_id_secret(
238        &self,
239        crypto: &impl OpenMlsCrypto,
240        ciphersuite: Ciphersuite,
241    ) -> Result<GenerationIdSecret, VirtualClientsError> {
242        let secret = self
243            .0
244            .derive_secret(crypto, ciphersuite, GENERATION_ID_LABEL)?;
245        Ok(GenerationIdSecret(secret))
246    }
247}
248
249/// Per-derivation-epoch secret used to derive the FF1 PRP key for
250/// `reuse_guard` values sent by this virtual client. Derived from
251/// [`EmulatorEpochSecret`] via [`EmulatorEpochSecret::derive_reuse_guard_secret`].
252#[derive(Debug, Serialize, Deserialize)]
253pub(crate) struct ReuseGuardSecret(Secret);
254
255impl ReuseGuardSecret {
256    /// Test-only constructor from raw bytes.
257    #[cfg(test)]
258    pub(crate) fn from_secret_for_tests(secret: Secret) -> Self {
259        Self(secret)
260    }
261
262    /// Derive the 16-byte FF1 PRP key for a single application message:
263    ///
264    /// ```text
265    /// prp_key = ExpandWithLabel(reuse_guard_secret, "reuse guard",
266    ///                           key_schedule_nonce, 16)
267    /// ```
268    ///
269    /// `ciphersuite` is the emulation group's ciphersuite, stored on
270    /// [`VcDerivationEpochState`].
271    pub(crate) fn derive_prp_key(
272        &self,
273        crypto: &impl OpenMlsCrypto,
274        ciphersuite: Ciphersuite,
275        key_schedule_nonce: &[u8],
276    ) -> Result<[u8; PRP_KEY_LEN], VirtualClientsError> {
277        let key = self.0.kdf_expand_label(
278            crypto,
279            ciphersuite,
280            REUSE_GUARD_PRP_KEY_LABEL,
281            key_schedule_nonce,
282            PRP_KEY_LEN,
283        )?;
284        key.as_slice()
285            .try_into()
286            .map_err(|_| VirtualClientsError::HashOutputLengthMismatch {
287                actual_length: key.as_slice().len(),
288                expected_length: PRP_KEY_LEN,
289            })
290    }
291}
292
293/// Per-derivation-epoch secret used to derive generation IDs for DS
294/// collision detection (mls-virtual-clients draft, "Coordinating ratchet
295/// generations with the DS" section). Derived from [`EmulatorEpochSecret`]
296/// via [`EmulatorEpochSecret::derive_generation_id_secret`].
297#[derive(Debug, Serialize, Deserialize)]
298pub(crate) struct GenerationIdSecret(Secret);
299
300impl GenerationIdSecret {
301    /// Derive the [`GenerationId`] for a message sent with the given
302    /// [`PrivateMessageContext`]:
303    ///
304    /// ```text
305    /// generation_id = ExpandWithLabel(generation_id_secret, "generation id",
306    ///                                 PrivateMessageContext, Kdf.Nh)
307    /// ```
308    ///
309    /// `ciphersuite` is the emulation group's ciphersuite, the same one the
310    /// `generation_id_secret` was derived under.
311    fn derive_generation_id(
312        &self,
313        crypto: &impl OpenMlsCrypto,
314        ciphersuite: Ciphersuite,
315        context: &PrivateMessageContext<'_>,
316    ) -> Result<GenerationId, VirtualClientsError> {
317        let context_bytes = context.tls_serialize_detached()?;
318        let generation_id = self.0.kdf_expand_label(
319            crypto,
320            ciphersuite,
321            GENERATION_ID_EXPAND_LABEL,
322            &context_bytes,
323            ciphersuite.hash_length(),
324        )?;
325        Ok(GenerationId(generation_id.as_slice().to_vec().into()))
326    }
327}
328
329/// Which ratchet a `PrivateMessageContext` refers to
330/// (mls-virtual-clients draft `RatchetType`):
331///
332/// ```text
333/// enum {
334///   reserved(0),
335///   application(1),
336///   handshake(2),
337///   (255)
338/// } RatchetType
339/// ```
340///
341/// [`Application`](Self::Application) covers application messages, and
342/// [`Handshake`](Self::Handshake) covers proposals and commits framed as
343/// PrivateMessages in a higher-level group. Both draw a generation ID from
344/// their respective per-leaf ratchet.
345#[derive(Debug, Clone, Copy, PartialEq, Eq, TlsSize, TlsSerialize)]
346#[repr(u8)]
347pub enum RatchetType {
348    /// The per-leaf application-message ratchet.
349    Application = 1,
350    /// The per-leaf handshake-message ratchet.
351    Handshake = 2,
352}
353
354/// Context a [`GenerationId`] is derived over (mls-virtual-clients draft):
355///
356/// ```text
357/// struct {
358///   opaque group_id<V>;
359///   uint64 epoch;
360///   uint32 generation;
361///   RatchetType ratchet_type;
362/// } PrivateMessageContext
363/// ```
364///
365/// `group_id` and `epoch` identify the higher-level group and its epoch at
366/// the time the message is sent, `generation` is the ratchet generation used
367/// for encryption, and `ratchet_type` distinguishes the application and
368/// handshake ratchets. Only ever serialized as a derivation context, never
369/// parsed back, so it borrows its `group_id` and needs serialization only.
370#[derive(Debug, TlsSize, TlsSerialize)]
371pub(crate) struct PrivateMessageContext<'a> {
372    group_id: VLByteSlice<'a>,
373    epoch: u64,
374    generation: u32,
375    ratchet_type: RatchetType,
376}
377
378/// A per-message generation ID a virtual client attaches to a fanned-out
379/// PrivateMessage so a strongly-consistent DS can detect generation
380/// collisions between siblings, per higher-level group, per higher-level
381/// group epoch, and per ratchet type (mls-virtual-clients draft).
382///
383/// Derived from the derivation epoch's `GenerationIdSecret` over a
384/// `PrivateMessageContext`. The value is opaque to the application: it is
385/// produced by [`MlsGroup::create_unconfirmed_message`] and handed to the DS,
386/// which compares it for equality across siblings.
387///
388/// [`MlsGroup::create_unconfirmed_message`]: crate::group::MlsGroup::create_unconfirmed_message
389#[derive(Debug, Clone, PartialEq, Eq, TlsSize, TlsSerialize, TlsDeserializeBytes)]
390pub struct GenerationId(VLByteVec);
391
392impl GenerationId {
393    /// The raw generation-ID bytes the application hands to the DS.
394    pub fn as_slice(&self) -> &[u8] {
395        self.0.as_slice()
396    }
397}
398
399/// The virtual-clients derivation info carried in the leaf node's
400/// `app_data_dictionary` extension under [`VC_COMPONENT_ID`]
401/// (mls-virtual-clients draft):
402///
403/// ```text
404/// struct {
405///   opaque epoch_id<V>;
406///   opaque ciphertext<V>;
407/// } DerivationInfo
408/// ```
409///
410/// `ciphertext` is the AEAD-wrapped [`DerivationInfoTbe`], encrypted in the
411/// emulation group's ciphersuite with key and nonce derived from the
412/// per-epoch [`EpochEncryptionKey`] and the carrying leaf's serialized
413/// `encryption_key`, with `epoch_id` as AAD.
414#[derive(Debug, TlsSize, TlsSerialize, TlsDeserializeBytes)]
415pub(crate) struct DerivationInfo {
416    epoch_id: EpochId,
417    ciphertext: VLByteVec,
418}
419
420impl DerivationInfo {
421    /// Encrypt `tbe` under the per-epoch AEAD key, binding it to the leaf
422    /// that carries the resulting derivation info via the leaf's serialized
423    /// `encryption_key` (the key/nonce derivation context) and to
424    /// `epoch_id` (the AAD).
425    pub(crate) fn encrypt(
426        crypto: &impl OpenMlsCrypto,
427        ciphersuite: Ciphersuite,
428        key: &EpochEncryptionKey,
429        epoch_id: EpochId,
430        leaf_encryption_key: &[u8],
431        tbe: &DerivationInfoTbe,
432    ) -> Result<Self, VirtualClientsError> {
433        let (aead_key, aead_nonce) =
434            key.derive_key_nonce(crypto, ciphersuite, leaf_encryption_key)?;
435        let payload = tbe.tls_serialize_detached()?;
436        let ciphertext = crypto.aead_encrypt(
437            ciphersuite.aead_algorithm(),
438            aead_key.as_slice(),
439            payload.as_slice(),
440            aead_nonce.as_slice(),
441            epoch_id.0.as_slice(),
442        )?;
443        Ok(Self {
444            epoch_id,
445            ciphertext: ciphertext.into(),
446        })
447    }
448
449    pub(crate) fn epoch_id(&self) -> &EpochId {
450        &self.epoch_id
451    }
452
453    /// Decrypt the wrapped [`DerivationInfoTbe`]. `leaf_encryption_key` is
454    /// the serialized `encryption_key` of the leaf node that carries this
455    /// derivation info.
456    pub(crate) fn decrypt(
457        &self,
458        crypto: &impl OpenMlsCrypto,
459        ciphersuite: Ciphersuite,
460        key: &EpochEncryptionKey,
461        leaf_encryption_key: &[u8],
462        operation_type: VirtualClientOperationType,
463    ) -> Result<DerivationInfoTbe, VirtualClientsError> {
464        let (aead_key, aead_nonce) =
465            key.derive_key_nonce(crypto, ciphersuite, leaf_encryption_key)?;
466        let plaintext = crypto
467            .aead_decrypt(
468                ciphersuite.aead_algorithm(),
469                aead_key.as_slice(),
470                self.ciphertext.as_slice(),
471                aead_nonce.as_slice(),
472                self.epoch_id.0.as_slice(),
473            )
474            .map_err(|e| {
475                log::error!("vc: aead decrypt derivation info failed: {e:?}");
476                VirtualClientsError::DerivationInfoDecryptionFailed
477            })?;
478        DerivationInfoTbe::deserialize_for_operation(&plaintext, operation_type)
479    }
480}
481
482/// Identifier of a derivation epoch's registered virtual-clients state.
483/// Derived deterministically from the emulation group's
484/// `safe_export_secret(VC_COMPONENT_ID)`, so every emulator client of a virtual
485/// client arrives at the same value for a given derivation epoch.
486#[derive(
487    Debug,
488    Clone,
489    PartialEq,
490    Eq,
491    PartialOrd,
492    Ord,
493    Serialize,
494    Deserialize,
495    TlsSize,
496    TlsSerialize,
497    TlsDeserializeBytes,
498)]
499pub struct EpochId(VLByteVec);
500
501impl EpochId {
502    /// Create an epoch ID from raw bytes.
503    pub fn new(bytes: Vec<u8>) -> Self {
504        Self(bytes.into())
505    }
506
507    /// The raw epoch-ID bytes.
508    pub fn as_bytes(&self) -> &[u8] {
509        self.0.as_slice()
510    }
511}
512
513/// Wire struct a virtual client hands to a sibling so the sibling can fetch
514/// and process the matching KeyPackage (mls-virtual-clients draft):
515///
516/// ```text
517/// struct {
518///   opaque key_package_ref<V>;
519///   CipherSuite cipher_suite;
520///   uint32 key_package_index;
521/// } KeyPackageInfo
522/// ```
523///
524/// `key_package_ref` is the [`KeyPackageRef`] (a [`HashReference`]) of the
525/// KeyPackage built by [`KeyPackageBuilder::build_vc_batch`]. `key_package_index`
526/// is the KeyPackage's position within the `key_package` operation batch: one
527/// operation secret covers the whole batch and each KeyPackage's seed is
528/// derived from it under this index.
529///
530/// [`HashReference`]: crate::ciphersuite::hash_ref::HashReference
531/// [`KeyPackageBuilder::build_vc_batch`]: crate::key_packages::KeyPackageBuilder::build_vc_batch
532#[derive(Debug, PartialEq, TlsSize, TlsSerialize, TlsDeserializeBytes)]
533pub struct KeyPackageInfo {
534    /// Hash reference of the virtual client's KeyPackage.
535    pub key_package_ref: KeyPackageRef,
536    /// Ciphersuite of the virtual client's KeyPackage.
537    pub cipher_suite: Ciphersuite,
538    /// Position of this KeyPackage within the operation batch.
539    pub key_package_index: u32,
540}
541
542/// Wire struct a virtual client uploads to a sibling so the sibling learns
543/// about the KeyPackages the virtual client published for a derivation epoch
544/// (mls-virtual-clients draft):
545///
546/// ```text
547/// struct {
548///   opaque epoch_id<V>;
549///   uint32 leaf_index;
550///   uint32 generation;
551///   KeyPackageInfo key_package_info<V>;
552/// } KeyPackageUpload
553/// ```
554///
555/// `epoch_id` identifies the derivation epoch the KeyPackages belong to.
556/// `leaf_index` is the uploading client's emulation-group leaf index at that
557/// epoch. The receiver stores this leaf index: the KeyPackage operation
558/// secret was allocated from the uploader's per-leaf ratchet, so a sibling
559/// rederiving the KeyPackage material must walk that same leaf's ratchet, not
560/// its own. `generation` is the single `key_package` operation generation
561/// consumed for the whole batch. `key_package_info` carries one
562/// [`KeyPackageInfo`] per uploaded KeyPackage, each with its index within the
563/// batch.
564#[derive(Debug, PartialEq, TlsSize, TlsSerialize, TlsDeserializeBytes)]
565pub struct KeyPackageUpload {
566    /// Derivation epoch the uploaded KeyPackages belong to.
567    pub epoch_id: EpochId,
568    /// Uploading client's emulation-group leaf index at that epoch.
569    pub leaf_index: LeafNodeIndex,
570    /// Operation-ratchet generation consumed for the whole batch.
571    pub generation: u32,
572    /// One entry per uploaded KeyPackage.
573    pub key_package_info: Vec<KeyPackageInfo>,
574}
575
576/// Per-`KeyPackageRef` material a sibling retains when it processes a
577/// [`KeyPackageUpload`]. It captures what the Welcome path needs to later
578/// rederive the KeyPackage's init and leaf-encryption keys without touching
579/// the operation tree: the per-KeyPackage seed secret, plus the derivation
580/// epoch, leaf index, generation, and batch index used to validate the leaf
581/// found in the ratchet tree.
582///
583/// The seed is pinned here at upload-processing time so the Welcome path stays
584/// independent of the operation tree's bounded out-of-order tolerance: a batch
585/// can hold more KeyPackages than that tolerance, and Welcomes can arrive in
586/// any order, yet every seed remains available because the single batch
587/// generation is consumed once and each seed is stored alongside its index.
588#[derive(Debug, Serialize, Deserialize)]
589pub struct RetainedKeyPackageMaterial {
590    /// Derivation epoch the KeyPackage belongs to.
591    pub epoch_id: EpochId,
592    /// Uploader's emulation-group leaf index, identifying the operation
593    /// ratchet the batch generation was allocated from.
594    pub leaf_index: LeafNodeIndex,
595    /// Operation-ratchet generation consumed for the whole batch.
596    pub generation: u32,
597    /// Ciphersuite of the KeyPackage.
598    pub key_package_ciphersuite: Ciphersuite,
599    /// Position of this KeyPackage within the batch.
600    pub key_package_index: u32,
601    /// Per-KeyPackage seed secret from which the init and leaf-encryption keys
602    /// are derived at Welcome time.
603    pub key_package_seed_secret: KeyPackageSeedSecret,
604}
605
606/// Reject a batch whose [`KeyPackageInfo`] entries are not all distinct.
607///
608/// Returns [`VirtualClientsError::DuplicateKeyPackageIndex`] if any
609/// `key_package_index` repeats, and
610/// [`VirtualClientsError::DuplicateKeyPackageRef`] if any `key_package_ref`
611/// repeats. A duplicate index would map two KeyPackages onto the same
612/// per-index seed, and a duplicate reference would have the second upload
613/// entry overwrite the first's retained material, so both are rejected before
614/// any state is loaded or any operation generation is consumed.
615fn validate_key_package_infos(infos: &[KeyPackageInfo]) -> Result<(), VirtualClientsError> {
616    let mut seen_indices = BTreeSet::new();
617    let mut seen_refs = BTreeSet::new();
618    for info in infos {
619        if !seen_indices.insert(info.key_package_index) {
620            return Err(VirtualClientsError::DuplicateKeyPackageIndex(
621                info.key_package_index,
622            ));
623        }
624        if !seen_refs.insert(&info.key_package_ref) {
625            return Err(VirtualClientsError::DuplicateKeyPackageRef);
626        }
627    }
628    Ok(())
629}
630
631/// Build a [`KeyPackageUpload`] for `epoch_id` from a batch's `generation` and
632/// its [`KeyPackageInfo`] entries, filling `leaf_index` from the
633/// [`VcDerivationEpochState`] stored for that epoch.
634///
635/// The virtual client calls this after building a batch of KeyPackages with
636/// [`KeyPackageBuilder::build_vc_batch`] to assemble the message it hands to
637/// its sibling. `generation` is the single `key_package` operation generation
638/// the batch consumed.
639///
640/// This describes a completed operation rather than starting a new one, so it
641/// takes the epoch explicitly. Pass the `epoch_id` and `generation` the batch
642/// reports, not a freshly resolved epoch: the emulation group may have moved on
643/// to a newer derivation epoch since the batch was built.
644///
645/// Returns [`VirtualClientsError::MissingDerivationEpochState`] if no state is
646/// registered for `epoch_id`.
647///
648/// [`KeyPackageBuilder::build_vc_batch`]: crate::key_packages::KeyPackageBuilder::build_vc_batch
649pub fn assemble_vc_key_package_upload<Storage: crate::storage::StorageProvider>(
650    storage: &Storage,
651    epoch_id: EpochId,
652    generation: u32,
653    key_package_info: Vec<KeyPackageInfo>,
654) -> Result<KeyPackageUpload, VirtualClientsError> {
655    validate_key_package_infos(&key_package_info)?;
656    let state: VcDerivationEpochState = storage
657        .vc_derivation_epoch_state(&epoch_id)
658        .map_err(|e| {
659            log::error!("vc: load derivation epoch state in assemble upload failed: {e:?}");
660            VirtualClientsError::StorageError
661        })?
662        .ok_or(VirtualClientsError::MissingDerivationEpochState)?;
663    Ok(KeyPackageUpload {
664        epoch_id,
665        leaf_index: state.leaf_index,
666        generation,
667        key_package_info,
668    })
669}
670
671/// Process a [`KeyPackageUpload`] received from a sibling virtual client.
672///
673/// Derives the batch's single `key_package` operation secret once from the
674/// uploader's leaf ratchet at `(epoch_id, leaf_index, generation)`, then
675/// stores the advanced operation tree and one [`RetainedKeyPackageMaterial`]
676/// per [`KeyPackageInfo`] (keyed by the info's [`KeyPackageRef`]) in a single
677/// atomic batch write.
678///
679/// The batch operation secret is derived under the emulation ciphersuite (the
680/// operation tree's ciphersuite). Each per-KeyPackage seed is imported from
681/// it into the ciphersuite the upload names for this KeyPackage. The init and
682/// leaf-encryption keys are later derived from each seed under the same
683/// ciphersuite at Welcome time. The operation secret is dropped once all seeds
684/// are derived. The batch generation is consumed in the tree exactly once.
685pub fn process_vc_key_package_upload<Provider: OpenMlsProvider>(
686    provider: &Provider,
687    upload: &KeyPackageUpload,
688) -> Result<(), VirtualClientsError> {
689    use crate::components::vc_operation_tree::OperationSecretTree;
690    use openmls_traits::storage::StorageProvider as _;
691
692    validate_key_package_infos(&upload.key_package_info)?;
693
694    let storage = provider.storage();
695    let crypto = provider.crypto();
696
697    let state: VcDerivationEpochState = storage
698        .vc_derivation_epoch_state(&upload.epoch_id)
699        .map_err(|e| {
700            log::error!("vc: load derivation epoch state in process upload failed: {e:?}");
701            VirtualClientsError::StorageError
702        })?
703        .ok_or(VirtualClientsError::MissingDerivationEpochState)?;
704    let mut operation_tree: OperationSecretTree = storage
705        .vc_operation_tree(&upload.epoch_id)
706        .map_err(|e| {
707            log::error!("vc: load operation tree in process upload failed: {e:?}");
708            VirtualClientsError::StorageError
709        })?
710        .ok_or(VirtualClientsError::MissingOperationTree)?;
711    let emulation_ciphersuite = state.emulation_ciphersuite;
712
713    // The KeyPackage operation context is empty, matching `build_vc_batch`.
714    let operation_secret = operation_tree.derive_operation_secret(
715        crypto,
716        emulation_ciphersuite,
717        &upload.epoch_id,
718        upload.leaf_index,
719        VirtualClientOperationType::KeyPackage,
720        upload.generation,
721        b"",
722    )?;
723
724    let mut materials = Vec::with_capacity(upload.key_package_info.len());
725    for info in &upload.key_package_info {
726        let key_package_seed_secret = operation_secret.derive_key_package_seed_secret(
727            crypto,
728            info.cipher_suite,
729            info.key_package_index,
730        )?;
731        let material = RetainedKeyPackageMaterial {
732            epoch_id: upload.epoch_id.clone(),
733            leaf_index: upload.leaf_index,
734            generation: upload.generation,
735            key_package_ciphersuite: info.cipher_suite,
736            key_package_index: info.key_package_index,
737            key_package_seed_secret,
738        };
739        materials.push((info.key_package_ref.clone(), material));
740    }
741
742    storage
743        .write_retained_key_package_material_batch(&upload.epoch_id, &operation_tree, &materials)
744        .map_err(|e| {
745            log::error!("vc: persist batch key package material in process upload failed: {e:?}");
746            VirtualClientsError::StorageError
747        })?;
748    Ok(())
749}
750
751/// Material a sibling emulator derives to join a higher-level group via a
752/// virtual client's KeyPackage.
753///
754/// Carried from the first Welcome stage (where the init private key decrypts
755/// the group secrets, before the ratchet tree is available) into staging
756/// (where the derived `encryption_keypair` becomes the joiner's leaf keypair
757/// and the recorded `(epoch_id, leaf_index, generation, key_package_index)`
758/// validate the leaf found in the tree). The keys are derived from the
759/// per-KeyPackage seed pinned in [`RetainedKeyPackageMaterial`], not by
760/// re-walking the operation tree.
761#[derive(Debug)]
762pub(crate) struct VcWelcomeMaterial {
763    /// The [`KeyPackageRef`] the welcome's encrypted group secrets addressed.
764    pub(crate) key_package_ref: KeyPackageRef,
765    /// Derivation epoch the KeyPackage belongs to.
766    pub(crate) epoch_id: EpochId,
767    /// Uploader's emulation-group leaf index, identifying the operation
768    /// ratchet the batch generation was allocated from.
769    pub(crate) leaf_index: LeafNodeIndex,
770    /// Operation-ratchet generation consumed for the whole batch.
771    pub(crate) generation: u32,
772    /// Position of this KeyPackage within the batch.
773    pub(crate) key_package_index: u32,
774    /// Init private key derived from the seed, used to decrypt the encrypted
775    /// group secrets.
776    pub(crate) init_private_key: openmls_traits::types::HpkePrivateKey,
777    /// Init key the welcome encrypted group secrets are encrypted with.
778    pub(crate) init_key: InitKey,
779    /// Leaf encryption keypair derived from the seed, used as the joiner's
780    /// leaf keypair.
781    pub(crate) encryption_keypair: EncryptionKeyPair,
782}
783
784/// One registration in an emulation group's log of derivation epochs, stored
785/// as its own row keyed by `(group_id, epoch_id)`.
786#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
787pub struct VcDerivationEpochLogEntry {
788    /// Position of this entry in the group's log, starting at 0.
789    pub(crate) sequence: u64,
790    /// The emulation group's own epoch at registration time.
791    pub(crate) group_epoch: GroupEpoch,
792    /// The derivation epoch id derived by that registration.
793    pub(crate) epoch_id: EpochId,
794    /// When the registration happened, in local wall-clock time.
795    pub(crate) registered_at: SystemTime,
796}
797
798impl VcDerivationEpochLogEntry {
799    /// Convert a [`RegisteredVcDerivationEpoch`] into a log entry. The record
800    /// was its group's only registration, so the entry takes sequence 0.
801    pub fn from_legacy_record(
802        group_epoch: GroupEpoch,
803        epoch_id: EpochId,
804        registered_at: SystemTime,
805    ) -> Self {
806        Self {
807            sequence: 0,
808            group_epoch,
809            epoch_id,
810            registered_at,
811        }
812    }
813
814    /// The derivation epoch this entry registered.
815    pub fn epoch_id(&self) -> &EpochId {
816        &self.epoch_id
817    }
818}
819
820/// Per-emulation-group log of the derivation epochs the group registered, in
821/// registration order with the newest at the back. The newest entry is the
822/// derivation epoch all new virtual-client operations of the group resolve to,
823/// which may be older than the group's current epoch.
824#[derive(Debug, Default)]
825pub(crate) struct VcDerivationEpochLog {
826    // In registration order, oldest at the front.
827    entries: VecDeque<VcDerivationEpochLogEntry>,
828}
829
830impl VcDerivationEpochLog {
831    /// Reconstruct the log of `group_id` from its stored entries. The log is
832    /// empty for a group that never registered a derivation epoch.
833    pub(crate) fn load<Storage: crate::storage::StorageProvider>(
834        storage: &Storage,
835        group_id: &GroupId,
836    ) -> Result<Self, Storage::Error> {
837        let mut entries: Vec<VcDerivationEpochLogEntry> =
838            storage.vc_derivation_epoch_log_entries(group_id)?;
839        entries.sort_unstable_by_key(|entry| entry.sequence);
840        // Entries are keyed by their epoch id in storage, so a duplicate id
841        // cannot come from storage. It would mean two registrations derived
842        // the same id, which the exporter derivation excludes.
843        debug_assert!(
844            entries
845                .iter()
846                .map(|entry| &entry.epoch_id)
847                .collect::<BTreeSet<_>>()
848                .len()
849                == entries.len(),
850            "duplicate derivation epoch id in log"
851        );
852        Ok(Self {
853            entries: entries.into(),
854        })
855    }
856
857    pub(crate) fn is_empty(&self) -> bool {
858        self.entries.is_empty()
859    }
860
861    /// The newest logged registration, or `None` if the log is empty.
862    pub(crate) fn newest(&self) -> Option<&VcDerivationEpochLogEntry> {
863        self.entries.back()
864    }
865
866    /// Append a registration of `epoch_id` for `group_epoch`, timestamped now
867    /// and sequenced after the current newest entry. Returns a clone of the
868    /// appended entry for the caller to persist.
869    fn push(&mut self, group_epoch: GroupEpoch, epoch_id: EpochId) -> VcDerivationEpochLogEntry {
870        let sequence = self
871            .entries
872            .back()
873            .map_or(0, |entry| entry.sequence.saturating_add(1));
874        let entry = VcDerivationEpochLogEntry {
875            sequence,
876            group_epoch,
877            epoch_id,
878            registered_at: SystemTime::now(),
879        };
880        self.entries.push_back(entry.clone());
881        entry
882    }
883
884    /// Drop the oldest entries until at most `max_entries` are left, and return
885    /// the epochs of the dropped entries. Never drops the newest entry, so the
886    /// group keeps a derivation epoch to operate on.
887    pub(crate) fn shrink_to(&mut self, max_entries: usize) -> Vec<EpochId> {
888        let excess = self.entries.len().saturating_sub(max_entries.max(1));
889        self.drop_oldest(excess)
890    }
891
892    /// Drop every entry superseded before `cutoff` and return the epochs of
893    /// the dropped entries. An entry is superseded when its successor is
894    /// registered, so entry `i` goes when entry `i + 1` was registered before
895    /// `cutoff`. The newest entry has no successor and never drops.
896    pub(crate) fn drop_superseded_before(&mut self, cutoff: SystemTime) -> Vec<EpochId> {
897        let count = self
898            .entries
899            .iter()
900            .skip(1)
901            .rposition(|successor| successor.registered_at < cutoff)
902            .map_or(0, |index| index + 1);
903        self.drop_oldest(count)
904    }
905
906    /// Drop the `count` oldest entries, keeping the newest one regardless, and
907    /// return their epochs. Each epoch appears in at most one entry.
908    fn drop_oldest(&mut self, count: usize) -> Vec<EpochId> {
909        let droppable = self.entries.len().saturating_sub(1);
910        self.entries
911            .drain(0..count.min(droppable))
912            .map(|entry| entry.epoch_id)
913            .collect()
914    }
915}
916
917/// Registration record of the storage layout that preceded the
918/// derivation-epoch log. Only for decoding stored records and converting them
919/// with [`VcDerivationEpochLogEntry::from_legacy_record`].
920#[deprecated(
921    since = "0.9.0",
922    note = "migration-only: decode pre-log registration records and convert them with \
923            `VcDerivationEpochLogEntry::from_legacy_record`. Will be removed in 0.10.0."
924)]
925#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
926pub struct RegisteredVcDerivationEpoch {
927    /// The emulation group's own epoch at registration time.
928    pub group_epoch: GroupEpoch,
929    /// The derivation epoch id derived by that registration.
930    pub epoch_id: EpochId,
931}
932
933/// The newest derivation epoch registered for the emulation group
934/// `emulation_group_id`, or `None` if none was registered yet.
935///
936/// Read from storage, so the result reflects the state at the time of the
937/// call.
938pub(crate) fn newest_vc_derivation_epoch<Storage: crate::storage::StorageProvider>(
939    storage: &Storage,
940    emulation_group_id: &GroupId,
941) -> Result<Option<EpochId>, Storage::Error> {
942    let entries: Vec<VcDerivationEpochLogEntry> =
943        storage.vc_derivation_epoch_log_entries(emulation_group_id)?;
944    Ok(entries
945        .into_iter()
946        .max_by_key(|entry| entry.sequence)
947        .map(|entry| entry.epoch_id))
948}
949
950/// Resolve the derivation epoch a new virtual-client operation must use: the
951/// newest one registered for the emulation group `emulation_group_id`.
952///
953/// The draft requires every new operation to use the newest derivation epoch of
954/// the acting client's current emulation-group state, so the epoch is never a
955/// parameter of an operation. An operation carried by a commit that itself
956/// creates a new derivation epoch still resolves against the commit's input
957/// state, because the new epoch is only registered when that commit is merged.
958///
959/// Returns [`VirtualClientsError::NoDerivationEpoch`] when no derivation epoch
960/// is registered, which is the case for every group that is not an emulation
961/// group.
962pub(crate) fn require_newest_vc_derivation_epoch<Storage: crate::storage::StorageProvider>(
963    storage: &Storage,
964    emulation_group_id: &GroupId,
965) -> Result<EpochId, VirtualClientsError> {
966    newest_vc_derivation_epoch(storage, emulation_group_id)
967        .map_err(|e| {
968            log::error!("vc: load newest derivation epoch for a new operation failed: {e:?}");
969            VirtualClientsError::StorageError
970        })?
971        .ok_or(VirtualClientsError::NoDerivationEpoch)
972}
973
974/// The emulation-group coordinates of the group epoch a derivation epoch is
975/// registered for. All values describe the *target* epoch, which for a merge is
976/// the epoch the commit moves the group into, not the one it is merged from.
977pub(crate) struct VcDerivationEpochParams<'a> {
978    /// Group id of the emulation group.
979    pub(crate) group_id: &'a GroupId,
980    /// Ciphersuite of the emulation group.
981    pub(crate) ciphersuite: Ciphersuite,
982    /// The emulation group's epoch this derivation epoch is sourced from.
983    pub(crate) group_epoch: GroupEpoch,
984    /// The registering client's own leaf index in the emulation group.
985    pub(crate) own_leaf_index: LeafNodeIndex,
986    /// Number of leaves in the emulation group's ratchet tree.
987    pub(crate) tree_size: TreeSize,
988    /// How many derivation epochs the group's log may keep.
989    pub(crate) retention_policy: VcDerivationEpochRetentionPolicy,
990}
991
992impl<'a> VcDerivationEpochParams<'a> {
993    /// Read the coordinates off the emulation group's public state. The caller
994    /// supplies `own_leaf_index` and the retention policy, which the public
995    /// state does not carry.
996    ///
997    /// For a merge, pass the state after the staged diff was merged, so the
998    /// coordinates describe the epoch the commit moves the group into.
999    pub(crate) fn for_public_group(
1000        public_group: &'a crate::group::PublicGroup,
1001        own_leaf_index: LeafNodeIndex,
1002        retention_policy: VcDerivationEpochRetentionPolicy,
1003    ) -> Self {
1004        Self {
1005            group_id: public_group.group_id(),
1006            ciphersuite: public_group.ciphersuite(),
1007            group_epoch: public_group.group_context().epoch(),
1008            own_leaf_index,
1009            tree_size: public_group.tree_size(),
1010            retention_policy,
1011        }
1012    }
1013}
1014
1015/// Derive and persist the virtual-clients derivation-epoch state for one epoch
1016/// of an emulation group.
1017///
1018/// Sources the per-derivation-epoch root secret by puncturing `export_tree`
1019/// under [`VC_COMPONENT_ID`], derives the [`EpochId`], the AEAD key, the epoch
1020/// base secret and the reuse-guard and generation-id secrets, builds the
1021/// per-epoch operation secret tree (sized like the emulation group's ratchet
1022/// tree), and persists the tree, the per-epoch state and the appended
1023/// derivation-epoch log entry. Returns the derived [`EpochId`].
1024///
1025/// Appending to the log applies the group's retention policy (see
1026/// [`VcDerivationEpochRetentionPolicy`]), which may delete the state of older
1027/// derivation epochs.
1028///
1029/// The caller owns `export_tree` and is responsible for persisting it after
1030/// this call, so that the puncture is not lost. A `None` export tree fails with
1031/// [`RegisterVcDerivationEpochError::MissingApplicationExportTree`]: merging
1032/// without registering would silently keep the old derivation epoch active,
1033/// which breaks the post-compromise guarantees of a membership change.
1034///
1035/// A registration consumes the forward-secure exporter, so it can derive
1036/// state at most once per group epoch. A repeated call for an
1037/// already-registered group epoch returns the recorded [`EpochId`] and leaves
1038/// the persisted operation secret tree untouched. The repeat still punctures
1039/// `export_tree` when it is handed a fresh, unpunctured tree for that epoch,
1040/// as a retried Welcome join does. Without the puncture the caller would
1041/// persist a tree that can re-derive the consumed secret.
1042pub(crate) fn register_vc_derivation_epoch<
1043    Crypto: OpenMlsCrypto,
1044    Storage: crate::storage::StorageProvider,
1045>(
1046    crypto: &Crypto,
1047    storage: &Storage,
1048    export_tree: Option<&mut ApplicationExportTree>,
1049    params: VcDerivationEpochParams<'_>,
1050) -> Result<EpochId, RegisterVcDerivationEpochError<Storage::Error>> {
1051    let VcDerivationEpochParams {
1052        group_id,
1053        ciphersuite,
1054        group_epoch,
1055        own_leaf_index,
1056        tree_size,
1057        retention_policy,
1058    } = params;
1059    let export_tree =
1060        export_tree.ok_or(RegisterVcDerivationEpochError::MissingApplicationExportTree)?;
1061
1062    let mut log = VcDerivationEpochLog::load(storage, group_id).map_err(|e| {
1063        log::error!("vc: load derivation epoch log before registration failed: {e:?}");
1064        RegisterVcDerivationEpochError::Storage(e)
1065    })?;
1066
1067    // Puncture before consulting the log. A repeat for a registered epoch can
1068    // hold a fresh, unpunctured tree, and returning early on the log alone
1069    // would let the caller persist that tree with the consumed secret still
1070    // derivable.
1071    let bytes = match export_tree.safe_export_secret(crypto, ciphersuite, VC_COMPONENT_ID) {
1072        Ok(bytes) => bytes,
1073        Err(ApplicationExportTreeError::PuncturedInput) => {
1074            // The tree in hand is already consumed, so this is an in-process
1075            // repeat of a completed registration and the log must agree.
1076            if let Some(newest) = log.newest() {
1077                if newest.group_epoch == group_epoch {
1078                    return Ok(newest.epoch_id.clone());
1079                }
1080            }
1081            return Err(RegisterVcDerivationEpochError::ApplicationExportTree(
1082                ApplicationExportTreeError::PuncturedInput,
1083            ));
1084        }
1085        Err(e) => return Err(e.into()),
1086    };
1087    let emulator_epoch_secret = EmulatorEpochSecret::new(bytes.as_slice());
1088    let epoch_id = emulator_epoch_secret.derive_epoch_id(crypto, ciphersuite)?;
1089    if let Some(newest) = log.newest() {
1090        if newest.group_epoch == group_epoch && newest.epoch_id == epoch_id {
1091            // A retry with identical key material, for example a Welcome join
1092            // repeated because the first one committed but the application
1093            // crashed before recording its success. The per-epoch state is
1094            // already persisted, only the fresh tree needed puncturing.
1095            return Ok(newest.epoch_id.clone());
1096        }
1097    }
1098    let epoch_encryption_key =
1099        emulator_epoch_secret.derive_epoch_encryption_key(crypto, ciphersuite)?;
1100    let epoch_base_secret = emulator_epoch_secret.derive_epoch_base_secret(crypto, ciphersuite)?;
1101    let reuse_guard_secret =
1102        emulator_epoch_secret.derive_reuse_guard_secret(crypto, ciphersuite)?;
1103    let generation_id_secret =
1104        emulator_epoch_secret.derive_generation_id_secret(crypto, ciphersuite)?;
1105    let operation_tree = OperationSecretTree::new(epoch_base_secret, tree_size);
1106    let state = VcDerivationEpochState::new(
1107        own_leaf_index,
1108        epoch_encryption_key,
1109        reuse_guard_secret,
1110        generation_id_secret,
1111        tree_size,
1112        ciphersuite,
1113    );
1114    let entry = log.push(group_epoch, epoch_id.clone());
1115    let dropped = log.shrink_to(retention_policy.max_epochs().unwrap_or(usize::MAX));
1116
1117    storage
1118        .write_vc_operation_tree(&epoch_id, &operation_tree)
1119        .map_err(|e| {
1120            log::error!("vc: persist operation tree at registration failed: {e:?}");
1121            RegisterVcDerivationEpochError::Storage(e)
1122        })?;
1123    storage
1124        .write_vc_derivation_epoch_state(&epoch_id, &state)
1125        .map_err(|e| {
1126            log::error!("vc: persist derivation epoch state at registration failed: {e:?}");
1127            RegisterVcDerivationEpochError::Storage(e)
1128        })?;
1129    storage
1130        .write_vc_derivation_epoch_log_entry(group_id, &epoch_id, &entry)
1131        .map_err(|e| {
1132            log::error!("vc: persist derivation epoch log entry at registration failed: {e:?}");
1133            RegisterVcDerivationEpochError::Storage(e)
1134        })?;
1135    if !dropped.is_empty() {
1136        storage
1137            .delete_vc_derivation_epoch_log_entries(group_id, &dropped)
1138            .map_err(|e| {
1139                log::error!("vc: prune derivation epoch log at registration failed: {e:?}");
1140                RegisterVcDerivationEpochError::Storage(e)
1141            })?;
1142    }
1143    // The sweep releases the epochs that just dropped out of the log, unless
1144    // something else still references them, and collects any orphans earlier
1145    // crashes left behind.
1146    storage
1147        .delete_unreferenced_vc_derivation_epoch_states::<EpochId>()
1148        .map_err(|e| {
1149            log::error!("vc: release pruned derivation epochs at registration failed: {e:?}");
1150            RegisterVcDerivationEpochError::Storage(e)
1151        })?;
1152
1153    Ok(epoch_id)
1154}
1155
1156/// The binding of one epoch of a higher-level group to the derivation epoch
1157/// whose virtual-client LeafNode was active at that epoch, stored as its own
1158/// row keyed by `(group_id, group_epoch)`.
1159///
1160/// Bindings are kept per group epoch because a delayed PrivateMessage from a
1161/// past higher-level epoch has to be deprotected with the derivation epoch
1162/// that was bound then. A group retains as many bindings as its message
1163/// secrets store keeps past epochs.
1164#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1165pub struct VcEmulationBinding {
1166    /// The higher-level group's epoch this binding is stored for.
1167    pub(crate) group_epoch: GroupEpoch,
1168    /// The derivation epoch bound at that group epoch.
1169    pub(crate) epoch_id: EpochId,
1170}
1171
1172impl VcEmulationBinding {
1173    /// Build the binding of `group_epoch` to `epoch_id` from one entry of a
1174    /// [`VcEmulationBindings`] record.
1175    pub fn from_legacy_record(group_epoch: GroupEpoch, epoch_id: EpochId) -> Self {
1176        Self {
1177            group_epoch,
1178            epoch_id,
1179        }
1180    }
1181
1182    /// The derivation epoch this binding names.
1183    pub fn epoch_id(&self) -> &EpochId {
1184        &self.epoch_id
1185    }
1186
1187    pub(crate) fn into_epoch_id(self) -> EpochId {
1188        self.epoch_id
1189    }
1190}
1191
1192/// Per-group bindings record of the storage layout that preceded per-epoch
1193/// [`VcEmulationBinding`] rows. Only for decoding stored records and
1194/// converting their entries with [`VcEmulationBinding::from_legacy_record`].
1195#[deprecated(
1196    since = "0.9.0",
1197    note = "migration-only: decode pre-row bindings records and convert their entries with \
1198            `VcEmulationBinding::from_legacy_record`. Will be removed in 0.10.0."
1199)]
1200#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1201pub struct VcEmulationBindings {
1202    // In order of insertion, oldest at the front.
1203    bindings: VecDeque<(GroupEpoch, EpochId)>,
1204}
1205
1206#[allow(deprecated)]
1207impl VcEmulationBindings {
1208    /// The `(group_epoch, epoch_id)` pairs of the record, oldest first.
1209    pub fn into_entries(self) -> Vec<(GroupEpoch, EpochId)> {
1210        self.bindings.into()
1211    }
1212}
1213
1214/// Bind `group_epoch` of the higher-level group `group_id` to `epoch_id`, then
1215/// prune the group's bindings to at most `max_entries` by deleting the ones
1216/// with the lowest group epochs. `max_entries` follows the group's
1217/// message-secrets retention, so bindings age out in lockstep with the message
1218/// secrets they are needed for.
1219pub(crate) fn write_vc_emulation_binding_with_pruning<Storage: crate::storage::StorageProvider>(
1220    storage: &Storage,
1221    group_id: &GroupId,
1222    group_epoch: GroupEpoch,
1223    epoch_id: EpochId,
1224    max_entries: usize,
1225) -> Result<(), Storage::Error> {
1226    let binding = VcEmulationBinding {
1227        group_epoch,
1228        epoch_id: epoch_id.clone(),
1229    };
1230    storage.write_vc_emulation_binding(group_id, &group_epoch, &epoch_id, &binding)?;
1231    let mut bindings: Vec<VcEmulationBinding> = storage.vc_emulation_bindings(group_id)?;
1232    if bindings.len() > max_entries {
1233        bindings.sort_unstable_by_key(|binding| binding.group_epoch.as_u64());
1234        let stale: Vec<GroupEpoch> = bindings[..bindings.len() - max_entries]
1235            .iter()
1236            .map(|binding| binding.group_epoch)
1237            .collect();
1238        storage.delete_vc_emulation_bindings(group_id, &stale)?;
1239    }
1240    Ok(())
1241}
1242
1243/// Per-epoch secret from which the sender derives the AEAD key and nonce
1244/// that wrap the [`DerivationInfoTbe`] in the leaf's `app_data_dictionary`
1245/// entry, and the receiver the same pair to unwrap it:
1246///
1247/// ```text
1248/// derivation_info_key = ExpandWithLabel(epoch_encryption_key, "key",
1249///                                       encryption_key, AEAD.Nk)
1250/// derivation_info_nonce = ExpandWithLabel(epoch_encryption_key, "nonce",
1251///                                         encryption_key, AEAD.Nn)
1252/// ```
1253///
1254/// where `encryption_key` is the serialized `encryption_key` field of the
1255/// LeafNode carrying the derivation info. Every operation produces a fresh
1256/// leaf encryption key, so each wrap uses a distinct key-nonce pair.
1257/// Derived from the emulation group's `safe_export_secret(VC_COMPONENT_ID)`
1258/// when the derivation epoch is registered.
1259#[derive(Debug, Serialize, Deserialize)]
1260pub(crate) struct EpochEncryptionKey(Secret);
1261
1262impl EpochEncryptionKey {
1263    /// Derive the AEAD key and nonce for one [`DerivationInfoTbe`] wrap,
1264    /// using the serialized `encryption_key` of the carrying leaf as the
1265    /// `ExpandWithLabel` context.
1266    fn derive_key_nonce(
1267        &self,
1268        crypto: &impl OpenMlsCrypto,
1269        ciphersuite: Ciphersuite,
1270        leaf_encryption_key: &[u8],
1271    ) -> Result<(Secret, Secret), VirtualClientsError> {
1272        let key = self.0.kdf_expand_label(
1273            crypto,
1274            ciphersuite,
1275            DERIVATION_INFO_KEY_LABEL,
1276            leaf_encryption_key,
1277            ciphersuite.aead_key_length(),
1278        )?;
1279        let nonce = self.0.kdf_expand_label(
1280            crypto,
1281            ciphersuite,
1282            DERIVATION_INFO_NONCE_LABEL,
1283            leaf_encryption_key,
1284            ciphersuite.aead_nonce_length(),
1285        )?;
1286        Ok((key, nonce))
1287    }
1288}
1289
1290/// Per-derivation-epoch state, persisted alongside the per-epoch operation
1291/// secret tree and keyed by [`EpochId`]. Bundles everything the library needs
1292/// to emit a VC commit for this epoch and to XOR application message nonces
1293/// with deterministic reuse guards.
1294///
1295/// This is the local storage encoding, not the draft's wire struct of the same
1296/// name. The draft's version carries the `epoch_id` and the operation secret
1297/// tree as fields, both of which are stored separately here and keyed by
1298/// [`EpochId`], and calls the leaf count `leaf_count` rather than
1299/// `emulation_group_size`.
1300#[derive(Debug, Serialize, Deserialize)]
1301pub struct VcDerivationEpochState {
1302    /// The registering client's leaf index in the emulation group at
1303    /// registration time. Sent in `DerivationInfoTbe` and used as the
1304    /// sender's `leaf_index_e` in the reuse-guard derivation.
1305    pub(crate) leaf_index: LeafNodeIndex,
1306    pub(crate) epoch_encryption_key: EpochEncryptionKey,
1307    pub(crate) reuse_guard_secret: ReuseGuardSecret,
1308    /// Used to derive the per-message [`GenerationId`] handed to the DS, via
1309    /// [`VcDerivationEpochState::derive_generation_id`].
1310    pub(crate) generation_id_secret: GenerationIdSecret,
1311    /// Number of leaves `N_e` in the emulation group at registration time.
1312    pub(crate) emulation_group_size: TreeSize,
1313    /// Ciphersuite of the emulation group at registration time. Used by
1314    /// the reuse-guard derivation.
1315    pub(crate) emulation_ciphersuite: Ciphersuite,
1316}
1317
1318impl VcDerivationEpochState {
1319    pub(crate) fn new(
1320        leaf_index: LeafNodeIndex,
1321        epoch_encryption_key: EpochEncryptionKey,
1322        reuse_guard_secret: ReuseGuardSecret,
1323        generation_id_secret: GenerationIdSecret,
1324        emulation_group_size: TreeSize,
1325        emulation_ciphersuite: Ciphersuite,
1326    ) -> Self {
1327        Self {
1328            leaf_index,
1329            epoch_encryption_key,
1330            reuse_guard_secret,
1331            generation_id_secret,
1332            emulation_group_size,
1333            emulation_ciphersuite,
1334        }
1335    }
1336
1337    /// Consume the state and return the fields needed by the
1338    /// commit-builder / commit-processing paths.
1339    pub(crate) fn into_parts(self) -> (LeafNodeIndex, EpochEncryptionKey, Ciphersuite) {
1340        (
1341            self.leaf_index,
1342            self.epoch_encryption_key,
1343            self.emulation_ciphersuite,
1344        )
1345    }
1346
1347    /// Derive the [`GenerationId`] for an application message sent in
1348    /// `group_id` at `epoch` with ratchet `generation`. The
1349    /// [`PrivateMessageContext`] is assembled from these inputs and the
1350    /// derivation epoch's [`GenerationIdSecret`], using the emulation group's
1351    /// ciphersuite.
1352    pub(crate) fn derive_generation_id(
1353        &self,
1354        crypto: &impl OpenMlsCrypto,
1355        group_id: &GroupId,
1356        epoch: GroupEpoch,
1357        generation: u32,
1358        ratchet_type: RatchetType,
1359    ) -> Result<GenerationId, VirtualClientsError> {
1360        let context = PrivateMessageContext {
1361            group_id: VLByteSlice(group_id.as_slice()),
1362            epoch: epoch.as_u64(),
1363            generation,
1364            ratchet_type,
1365        };
1366        self.generation_id_secret
1367            .derive_generation_id(crypto, self.emulation_ciphersuite, &context)
1368    }
1369
1370    /// Borrow the per-message inputs the framing layer needs to derive
1371    /// the PRP key and pick `x` for a reuse guard.
1372    pub(crate) fn reuse_guard_inputs(&self) -> crate::framing::EmulatorReuseGuardCtx<'_> {
1373        crate::framing::EmulatorReuseGuardCtx {
1374            reuse_guard_secret: &self.reuse_guard_secret,
1375            emulation_ciphersuite: self.emulation_ciphersuite,
1376            emulation_group_size: self.emulation_group_size,
1377            emulation_leaf_index: self.leaf_index,
1378        }
1379    }
1380}
1381
1382/// Per-operation secret from which the material for a single virtual-clients
1383/// operation (commit path, key package, application message) is derived.
1384/// Produced by the per-epoch Virtual Client Operation Secret Tree
1385/// ([`OperationSecretTree`]). Sender and receiver derive the same value
1386/// from the same per-epoch state.
1387///
1388/// [`OperationSecretTree`]: crate::components::vc_operation_tree::OperationSecretTree
1389#[derive(Debug, Serialize, Deserialize)]
1390pub struct OperationSecret(Secret);
1391
1392impl From<Secret> for OperationSecret {
1393    fn from(secret: Secret) -> Self {
1394        Self(secret)
1395    }
1396}
1397
1398/// Imports a secret from the emulation group's ciphersuite to the target ciphersuite.
1399///
1400/// The import MUST be performed even when the emulation group and target use the same ciphersuite.
1401fn import_secret(
1402    crypto: &impl OpenMlsCrypto,
1403    target_ciphersuite: Ciphersuite,
1404    source_secret: &Secret,
1405    label: &str,
1406    context: &[u8],
1407) -> Result<Secret, CryptoError> {
1408    let salt = Secret::from_slice(&[]);
1409    let target_prk = salt.hkdf_extract(crypto, target_ciphersuite, source_secret)?;
1410    target_prk.kdf_expand_label(
1411        crypto,
1412        target_ciphersuite,
1413        label,
1414        context,
1415        target_ciphersuite.hash_length(),
1416    )
1417}
1418
1419impl OperationSecret {
1420    /// The raw operation secret bytes.
1421    pub(crate) fn as_slice(&self) -> &[u8] {
1422        self.0.as_slice()
1423    }
1424
1425    /// Derive the `target_operation_secret` of a `leaf_node` operation: this
1426    /// operation secret imported into the higher-level group's ciphersuite:
1427    ///
1428    /// ```text
1429    /// target_operation_secret = ImportSecret(operation_secret,
1430    ///                                        "vc target operation",
1431    ///                                        TargetOperationContext)
1432    /// ```
1433    ///
1434    /// The context binds the target ciphersuite and the higher-level group's
1435    /// `group_id`, so one operation secret yields independent path material
1436    /// per target group. The commit path's encryption-key and path-generation
1437    /// secrets are derived from the returned [`TargetOperationSecret`], not
1438    /// from the operation secret directly. The committing emulator and the
1439    /// sibling recreating the commit derive the same value.
1440    pub(crate) fn derive_target_operation_secret(
1441        &self,
1442        crypto: &impl OpenMlsCrypto,
1443        target_ciphersuite: Ciphersuite,
1444        group_id: &GroupId,
1445    ) -> Result<TargetOperationSecret, VirtualClientsError> {
1446        let context = TargetOperationContext {
1447            cipher_suite: target_ciphersuite,
1448            group_id: VLByteSlice(group_id.as_slice()),
1449        }
1450        .tls_serialize_detached()?;
1451        let secret = import_secret(
1452            crypto,
1453            target_ciphersuite,
1454            &self.0,
1455            TARGET_OPERATION_LABEL,
1456            &context,
1457        )?;
1458        Ok(TargetOperationSecret(secret))
1459    }
1460
1461    /// Derive the per-KeyPackage seed secret for the KeyPackage at
1462    /// `key_package_index` within this operation's batch:
1463    ///
1464    /// ```text
1465    /// key_package_seed_secret = ImportSecret(operation_secret,
1466    ///                                        "vc key package seed",
1467    ///                                        KeyPackageSeedContext)
1468    /// ```
1469    ///
1470    /// The KeyPackage's init and leaf-encryption keys are then derived from the
1471    /// returned [`KeyPackageSeedSecret`], not from the operation secret
1472    /// directly, so a single `key_package` operation secret can cover a batch
1473    /// of KeyPackages with distinct key material.
1474    pub(crate) fn derive_key_package_seed_secret(
1475        &self,
1476        crypto: &impl OpenMlsCrypto,
1477        target_ciphersuite: Ciphersuite,
1478        key_package_index: u32,
1479    ) -> Result<KeyPackageSeedSecret, VirtualClientsError> {
1480        let context = KeyPackageSeedContext {
1481            cipher_suite: target_ciphersuite,
1482            key_package_index,
1483        }
1484        .tls_serialize_detached()?;
1485        let seed = import_secret(
1486            crypto,
1487            target_ciphersuite,
1488            &self.0,
1489            KEY_PACKAGE_SEED_LABEL,
1490            &context,
1491        )?;
1492        Ok(KeyPackageSeedSecret(seed))
1493    }
1494}
1495
1496/// `ExpandWithLabel` context for [`OperationSecret::derive_key_package_seed_secret`]
1497/// (mls-virtual-clients draft):
1498///
1499/// ```text
1500/// struct {
1501///   CipherSuite cipher_suite;
1502///   uint32 key_package_index;
1503/// } KeyPackageSeedContext
1504/// ```
1505///
1506/// Only ever serialized as a derivation context, never parsed back, so it
1507/// needs serialization only.
1508#[derive(Debug, TlsSize, TlsSerialize)]
1509struct KeyPackageSeedContext {
1510    cipher_suite: Ciphersuite,
1511    key_package_index: u32,
1512}
1513
1514/// Per-KeyPackage seed secret from which a single KeyPackage's init and
1515/// leaf-encryption keys are derived. Produced by
1516/// `OperationSecret::derive_key_package_seed_secret` for one index within a
1517/// `key_package` operation's batch. Persisted in [`RetainedKeyPackageMaterial`]
1518/// so the Welcome path can rederive the keys without re-walking the operation
1519/// tree.
1520#[derive(Debug, Serialize, Deserialize)]
1521pub struct KeyPackageSeedSecret(Secret);
1522
1523impl KeyPackageSeedSecret {
1524    pub(crate) fn derive_init_key_secret(
1525        &self,
1526        crypto: &impl OpenMlsCrypto,
1527        ciphersuite: Ciphersuite,
1528    ) -> Result<InitKeySecret, VirtualClientsError> {
1529        let init_key_secret = self.0.derive_secret(crypto, ciphersuite, INIT_KEY_LABEL)?;
1530        Ok(InitKeySecret(init_key_secret))
1531    }
1532
1533    pub(crate) fn derive_encryption_key_secret(
1534        &self,
1535        crypto: &impl OpenMlsCrypto,
1536        ciphersuite: Ciphersuite,
1537    ) -> Result<EncryptionKeySecret, VirtualClientsError> {
1538        let encryption_key_secret =
1539            self.0
1540                .derive_secret(crypto, ciphersuite, ENCRYPTION_KEY_LABEL)?;
1541        Ok(EncryptionKeySecret(encryption_key_secret))
1542    }
1543
1544    /// Derive the epoch-0 `epoch_secret` for a virtual-client-created group:
1545    ///
1546    /// ```text
1547    /// epoch_secret = DeriveSecret(key_package_seed_secret, "Group Creation")
1548    /// ```
1549    ///
1550    /// `ciphersuite` is the created (higher-level) group's ciphersuite, under
1551    /// which the resulting `epoch_secret` seeds the epoch key schedule. Both
1552    /// the creator and a reconstructing sibling derive it from the same seed,
1553    /// so the epoch secret never travels on the wire.
1554    pub(crate) fn derive_group_creation_secret(
1555        &self,
1556        crypto: &impl OpenMlsCrypto,
1557        ciphersuite: Ciphersuite,
1558    ) -> Result<Secret, VirtualClientsError> {
1559        Ok(self
1560            .0
1561            .derive_secret(crypto, ciphersuite, GROUP_CREATION_LABEL)?)
1562    }
1563}
1564
1565pub(crate) struct EncryptionKeySecret(Secret);
1566
1567impl EncryptionKeySecret {
1568    pub(crate) fn generate_encryption_key_pair(
1569        &self,
1570        crypto: &impl OpenMlsCrypto,
1571        ciphersuite: Ciphersuite,
1572    ) -> Result<EncryptionKeyPair, VirtualClientsError> {
1573        let hpke_config = ciphersuite.hpke_config();
1574        let key_pair = crypto.derive_hpke_keypair(hpke_config, self.0.as_slice())?;
1575        Ok(EncryptionKeyPair::from(key_pair))
1576    }
1577}
1578
1579pub(crate) struct InitKeySecret(Secret);
1580
1581impl InitKeySecret {
1582    pub(crate) fn generate_init_key_pair(
1583        &self,
1584        crypto: &impl OpenMlsCrypto,
1585        ciphersuite: Ciphersuite,
1586    ) -> Result<openmls_traits::types::HpkeKeyPair, VirtualClientsError> {
1587        let hpke_config = ciphersuite.hpke_config();
1588        let key_pair = crypto.derive_hpke_keypair(hpke_config, self.0.as_slice())?;
1589        Ok(key_pair)
1590    }
1591}
1592
1593pub(crate) struct PathGenerationSecret(Secret);
1594
1595impl From<PathGenerationSecret> for PathSecret {
1596    fn from(value: PathGenerationSecret) -> Self {
1597        value.0.into()
1598    }
1599}
1600
1601/// What virtual-clients operation a per-operation secret is being derived
1602/// for (mls-virtual-clients draft `VirtualClientOperationType`). Mixed into
1603/// the `OperationContext` of every operation-secret derivation so that
1604/// secrets derived for different operations cannot collide even if the other
1605/// fields happen to match.
1606///
1607/// The operation type does not travel on the wire. For the two operations
1608/// that produce a LeafNode, receivers infer it from that leaf's
1609/// `leaf_node_source`: `key_package` maps to [`KeyPackage`](Self::KeyPackage),
1610/// `update` and `commit` map to [`LeafNode`](Self::LeafNode).
1611///
1612/// [`Application`](Self::Application) secrets are not attached to a leaf. The
1613/// application takes them from the ratchet directly and publishes the
1614/// coordinates its siblings need, see the [`vc_application_secret`] module.
1615///
1616/// [`vc_application_secret`]: crate::components::vc_application_secret
1617#[derive(Debug, Clone, Copy, PartialEq, Eq, TlsSize, TlsSerialize, TlsDeserializeBytes)]
1618#[repr(u8)]
1619pub enum VirtualClientOperationType {
1620    /// Derivation of KeyPackage material for the virtual client.
1621    KeyPackage = 1,
1622    /// Derivation of LeafNode material for the virtual client (e.g. the
1623    /// leaf carried by a commit).
1624    LeafNode = 2,
1625    /// Derivation of application-message material for the virtual client.
1626    Application = 3,
1627}
1628
1629/// The external init secret carried by an external-commit LeafNode's
1630/// `DerivationInfoTBE` (mls-virtual-clients draft):
1631///
1632/// ```text
1633/// struct { opaque init_secret<V>; } ExternalInitSecret;
1634/// ```
1635///
1636/// It is the `init_secret` produced by external initialization
1637/// ({{Section 8.3 of RFC9420}}). A sibling emulator client processing the
1638/// external commit uses it as the new epoch's external init secret instead of
1639/// decapsulating from the previous epoch's `external_secret`, which it may not
1640/// hold.
1641#[derive(Clone, PartialEq, Eq, TlsSize, TlsSerialize, TlsDeserializeBytes)]
1642pub(crate) struct ExternalInitSecret(SecretVLByteVec);
1643
1644impl std::fmt::Debug for ExternalInitSecret {
1645    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1646        f.debug_struct("ExternalInitSecret")
1647            .field("init_secret", &"<redacted>")
1648            .finish()
1649    }
1650}
1651
1652impl ExternalInitSecret {
1653    pub(crate) fn from_slice(bytes: &[u8]) -> Self {
1654        Self(bytes.to_vec().into())
1655    }
1656
1657    pub(crate) fn as_slice(&self) -> &[u8] {
1658        self.0.as_slice()
1659    }
1660}
1661
1662/// ```text
1663/// struct {
1664///   CipherSuite cipher_suite;
1665///   opaque group_id<V>;
1666/// } TargetOperationContext
1667/// ```
1668#[derive(Debug, TlsSize, TlsSerialize)]
1669struct TargetOperationContext<'a> {
1670    cipher_suite: Ciphersuite,
1671    group_id: VLByteSlice<'a>,
1672}
1673
1674/// A leaf node operation secret imported into the higher-level group's ciphersuite.
1675///
1676/// Must be immediately deleted after the encryption key/path generation secrets are derived.
1677#[derive(Debug)]
1678pub(crate) struct TargetOperationSecret(Secret);
1679
1680impl TargetOperationSecret {
1681    pub(crate) fn derive_encryption_key_secret(
1682        &self,
1683        crypto: &impl OpenMlsCrypto,
1684        ciphersuite: Ciphersuite,
1685    ) -> Result<EncryptionKeySecret, VirtualClientsError> {
1686        let encryption_key_secret =
1687            self.0
1688                .derive_secret(crypto, ciphersuite, ENCRYPTION_KEY_LABEL)?;
1689        Ok(EncryptionKeySecret(encryption_key_secret))
1690    }
1691
1692    pub(crate) fn derive_path_generation_secret(
1693        &self,
1694        crypto: &impl OpenMlsCrypto,
1695        ciphersuite: Ciphersuite,
1696    ) -> Result<PathGenerationSecret, VirtualClientsError> {
1697        let path_generation_secret =
1698            self.0
1699                .derive_secret(crypto, ciphersuite, PATH_GENERATION_LABEL)?;
1700        Ok(PathGenerationSecret(path_generation_secret))
1701    }
1702}
1703
1704/// What a receiver derives from a sibling virtual client's commit in order to
1705/// recreate it: the emulation `epoch_id` the commit binds to, the per-commit
1706/// `operation_secret` the path is rederived from, and, for an external commit,
1707/// the carried `external_init_secret` (`None` for a regular commit).
1708///
1709/// Produced by `MlsGroup::load_vc_commit_material` and threaded into commit
1710/// staging as a single `Option`: either all three are present (a sibling VC
1711/// commit) or none are.
1712#[derive(Debug)]
1713pub(crate) struct VcCommitMaterial {
1714    /// Derivation epoch the commit's derivation info references.
1715    pub(crate) epoch_id: EpochId,
1716    /// Per-commit operation secret the receiver rederives the path from.
1717    pub(crate) operation_secret: OperationSecret,
1718    /// External init secret carried by an external commit, `None` otherwise.
1719    pub(crate) external_init_secret: Option<ExternalInitSecret>,
1720}
1721
1722/// AEAD plaintext attached to the leaf via the VC component
1723/// (mls-virtual-clients draft):
1724///
1725/// ```text
1726/// struct {
1727///   uint32 leaf_index;
1728///   uint32 generation;
1729///   select (LeafNode.leaf_node_source) {
1730///     case key_package: uint32 key_package_index;
1731///     case update:      struct{};
1732///     case commit:      optional<ExternalInitSecret> external_init_secret;
1733///   };
1734/// } DerivationInfoTBE
1735/// ```
1736///
1737/// `leaf_index` is the *emulation*-group leaf index of the sending virtual
1738/// client, *not* the leaf index in the group that carries this commit.
1739/// `generation` is the operation-ratchet generation the sender consumed for
1740/// this operation. `key_package_index`, present only for the `KeyPackage`
1741/// variant, is the KeyPackage's position within its `key_package` operation
1742/// batch. `external_init_secret`, present only for the commit variant, carries
1743/// the external init secret of an external commit (`Some`) and is absent
1744/// (`None`) for a regular commit.
1745#[derive(Debug, PartialEq, Eq)]
1746pub(crate) enum DerivationInfoTbe {
1747    /// Carried by `update` and `commit` leaves. No `key_package_index`. The
1748    /// codec treats the `LeafNode` operation type as the `commit` case (the
1749    /// only LeafNode-source leaf emitted today). `update`-proposal leaves are
1750    /// deferred and would need their own (field-less) codec branch.
1751    LeafNode {
1752        leaf_index: LeafNodeIndex,
1753        generation: u32,
1754        /// `Some` for an external commit, `None` for a regular commit.
1755        external_init_secret: Option<ExternalInitSecret>,
1756    },
1757    /// Carried by `key_package` leaves. Adds the position within the batch.
1758    KeyPackage {
1759        leaf_index: LeafNodeIndex,
1760        generation: u32,
1761        key_package_index: u32,
1762    },
1763}
1764
1765impl DerivationInfoTbe {
1766    /// The emulation-group leaf index of the sending virtual client.
1767    pub(crate) fn leaf_index(&self) -> LeafNodeIndex {
1768        match self {
1769            Self::LeafNode { leaf_index, .. } | Self::KeyPackage { leaf_index, .. } => *leaf_index,
1770        }
1771    }
1772
1773    /// The operation-ratchet generation the sender consumed.
1774    pub(crate) fn generation(&self) -> u32 {
1775        match self {
1776            Self::LeafNode { generation, .. } | Self::KeyPackage { generation, .. } => *generation,
1777        }
1778    }
1779
1780    /// The external init secret carried by an external-commit LeafNode, if any.
1781    /// Always `None` for `KeyPackage` and for regular (non-external) commits.
1782    pub(crate) fn external_init_secret(&self) -> Option<&ExternalInitSecret> {
1783        match self {
1784            Self::LeafNode {
1785                external_init_secret,
1786                ..
1787            } => external_init_secret.as_ref(),
1788            Self::KeyPackage { .. } => None,
1789        }
1790    }
1791
1792    /// Serialize the variant's fields in order, with no variant tag, matching
1793    /// the `DerivationInfoTBE` select. The TLS derive macros cannot express a
1794    /// tagless select, so this codec is written by hand.
1795    fn tls_serialize_detached(&self) -> Result<Vec<u8>, tls_codec::Error> {
1796        match self {
1797            Self::LeafNode {
1798                leaf_index,
1799                generation,
1800                external_init_secret,
1801            } => {
1802                let mut out = Vec::with_capacity(
1803                    leaf_index.tls_serialized_len()
1804                        + generation.tls_serialized_len()
1805                        + external_init_secret.tls_serialized_len(),
1806                );
1807                leaf_index.tls_serialize(&mut out)?;
1808                generation.tls_serialize(&mut out)?;
1809                external_init_secret.tls_serialize(&mut out)?;
1810                Ok(out)
1811            }
1812            Self::KeyPackage {
1813                leaf_index,
1814                generation,
1815                key_package_index,
1816            } => {
1817                let mut out = Vec::with_capacity(
1818                    leaf_index.tls_serialized_len()
1819                        + generation.tls_serialized_len()
1820                        + key_package_index.tls_serialized_len(),
1821                );
1822                leaf_index.tls_serialize(&mut out)?;
1823                generation.tls_serialize(&mut out)?;
1824                key_package_index.tls_serialize(&mut out)?;
1825                Ok(out)
1826            }
1827        }
1828    }
1829
1830    /// Deserialize the tagless select for the given operation type. The
1831    /// operation type stands in for the carrying leaf's `leaf_node_source`:
1832    /// [`KeyPackage`](VirtualClientOperationType::KeyPackage) parses the
1833    /// `KeyPackage` variant, [`LeafNode`](VirtualClientOperationType::LeafNode)
1834    /// the `LeafNode` variant. The plaintext must be consumed exactly.
1835    fn deserialize_for_operation(
1836        bytes: &[u8],
1837        operation_type: VirtualClientOperationType,
1838    ) -> Result<Self, VirtualClientsError> {
1839        let (leaf_index, rest) = LeafNodeIndex::tls_deserialize_bytes(bytes)?;
1840        let (generation, rest) = u32::tls_deserialize_bytes(rest)?;
1841        let (tbe, rest) = match operation_type {
1842            VirtualClientOperationType::KeyPackage => {
1843                let (key_package_index, rest) = u32::tls_deserialize_bytes(rest)?;
1844                (
1845                    Self::KeyPackage {
1846                        leaf_index,
1847                        generation,
1848                        key_package_index,
1849                    },
1850                    rest,
1851                )
1852            }
1853            // The `LeafNode` operation type is the `commit` case: it carries an
1854            // `optional<ExternalInitSecret>`. (`update`-proposal leaves are
1855            // deferred and would decode a field-less body instead.)
1856            VirtualClientOperationType::LeafNode => {
1857                let (external_init_secret, rest) =
1858                    Option::<ExternalInitSecret>::tls_deserialize_bytes(rest)?;
1859                (
1860                    Self::LeafNode {
1861                        leaf_index,
1862                        generation,
1863                        external_init_secret,
1864                    },
1865                    rest,
1866                )
1867            }
1868            VirtualClientOperationType::Application => {
1869                return Err(VirtualClientsError::DerivationInfoMalformed);
1870            }
1871        };
1872        if !rest.is_empty() {
1873            return Err(VirtualClientsError::DerivationInfoMalformed);
1874        }
1875        Ok(tbe)
1876    }
1877}
1878
1879/// Load the [`VcDerivationEpochState`] and [`OperationSecretTree`] for `epoch_id`,
1880/// mapping a missing entry to the matching `Missing*` error. Callers convert the
1881/// returned [`VirtualClientsError`] into their own error type.
1882///
1883/// [`OperationSecretTree`]: crate::components::vc_operation_tree::OperationSecretTree
1884pub(crate) fn load_vc_epoch_state_and_tree<Provider: OpenMlsProvider>(
1885    provider: &Provider,
1886    epoch_id: &EpochId,
1887) -> Result<
1888    (
1889        VcDerivationEpochState,
1890        crate::components::vc_operation_tree::OperationSecretTree,
1891    ),
1892    VirtualClientsError,
1893> {
1894    use openmls_traits::storage::StorageProvider as _;
1895
1896    let storage = provider.storage();
1897    let state = storage
1898        .vc_derivation_epoch_state(epoch_id)
1899        .map_err(|e| {
1900            log::error!("vc: load derivation epoch state failed: {e:?}");
1901            VirtualClientsError::StorageError
1902        })?
1903        .ok_or(VirtualClientsError::MissingDerivationEpochState)?;
1904    let operation_tree = storage
1905        .vc_operation_tree(epoch_id)
1906        .map_err(|e| {
1907            log::error!("vc: load operation tree failed: {e:?}");
1908            VirtualClientsError::StorageError
1909        })?
1910        .ok_or(VirtualClientsError::MissingOperationTree)?;
1911    Ok((state, operation_tree))
1912}
1913
1914/// Verify that the effective leaf about to carry a VC derivation-info entry
1915/// declares `AppDataDictionary` and lists [`VC_COMPONENT_ID`] in its
1916/// `AppComponents` entry, and return the resolved `AppDataDictionary`.
1917///
1918/// `caller_capabilities` and `caller_extensions` are the leaf parameters the
1919/// caller supplied for this operation. `current_leaf` is the leaf being
1920/// replaced, or `None` when there is none (a fresh KeyPackage, or an external
1921/// commit). The caller's `AppDataDictionary` is merged over the current
1922/// leaf's, with the caller winning on duplicate component ids, so injecting
1923/// the VC derivation-info preserves the `AppComponents` entry across
1924/// operations.
1925pub(crate) fn resolve_vc_leaf_dictionary(
1926    caller_capabilities: Option<&crate::treesync::node::leaf_node::Capabilities>,
1927    caller_extensions: Option<
1928        &crate::extensions::Extensions<crate::treesync::node::leaf_node::LeafNode>,
1929    >,
1930    current_leaf: Option<&crate::treesync::node::leaf_node::LeafNode>,
1931) -> Result<crate::extensions::AppDataDictionary, VirtualClientsError> {
1932    use crate::{
1933        component::{ComponentId, ComponentType},
1934        extensions::ExtensionType,
1935    };
1936    use tls_codec::DeserializeBytes as _;
1937
1938    let supports_app_data_dictionary = match caller_capabilities {
1939        Some(c) => c.extensions().contains(&ExtensionType::AppDataDictionary),
1940        None => current_leaf
1941            .map(|leaf| {
1942                leaf.capabilities()
1943                    .extensions()
1944                    .contains(&ExtensionType::AppDataDictionary)
1945            })
1946            .unwrap_or(false),
1947    };
1948    if !supports_app_data_dictionary {
1949        return Err(VirtualClientsError::AppDataDictionaryNotSupported);
1950    }
1951
1952    let mut resolved_dictionary = current_leaf
1953        .and_then(|leaf| leaf.extensions().app_data_dictionary())
1954        .map(|ext| ext.dictionary().clone())
1955        .unwrap_or_default();
1956    if let Some(caller_dict) = caller_extensions.and_then(|exts| exts.app_data_dictionary()) {
1957        for entry in caller_dict.dictionary().entries() {
1958            resolved_dictionary.insert(entry.id(), entry.data().to_vec());
1959        }
1960    }
1961
1962    let app_components_bytes = resolved_dictionary
1963        .get(&ComponentId::from(ComponentType::AppComponents))
1964        .map(<[u8]>::to_vec);
1965    let Some(app_components_bytes) = app_components_bytes else {
1966        return Err(VirtualClientsError::VcComponentNotListed);
1967    };
1968
1969    // The AppComponents body is `ComponentID supported_components<V>`, i.e.
1970    // a TLS-encoded variable-length vector of u16.
1971    let supported_components = Vec::<u16>::tls_deserialize_exact_bytes(&app_components_bytes)
1972        .map_err(|e| {
1973            log::error!("vc: AppComponents body failed to deserialize: {e:?}");
1974            VirtualClientsError::VcComponentNotListed
1975        })?;
1976    if !supported_components.contains(&VC_COMPONENT_ID) {
1977        return Err(VirtualClientsError::VcComponentNotListed);
1978    }
1979
1980    Ok(resolved_dictionary)
1981}
1982
1983/// Merge a virtual-clients derivation-info blob into `resolved_dictionary`
1984/// under [`VC_COMPONENT_ID`] and build the resulting leaf-node extensions.
1985///
1986/// Every other component id in `resolved_dictionary` (notably `AppComponents`)
1987/// is preserved, as is every non-`AppDataDictionary` extension the caller
1988/// supplied in `caller_extensions`. The rebuilt dictionary replaces any
1989/// `AppDataDictionary` entry already in that list.
1990pub(crate) fn merge_vc_derivation_info(
1991    caller_extensions: Option<
1992        &crate::extensions::Extensions<crate::treesync::node::leaf_node::LeafNode>,
1993    >,
1994    mut resolved_dictionary: crate::extensions::AppDataDictionary,
1995    derivation_info_bytes: Vec<u8>,
1996) -> Result<
1997    crate::extensions::Extensions<crate::treesync::node::leaf_node::LeafNode>,
1998    crate::error::LibraryError,
1999> {
2000    use crate::extensions::{AppDataDictionaryExtension, Extension, Extensions};
2001
2002    resolved_dictionary.insert(VC_COMPONENT_ID, derivation_info_bytes);
2003    let vc_extension =
2004        Extension::AppDataDictionary(AppDataDictionaryExtension::new(resolved_dictionary));
2005
2006    let other_extensions = caller_extensions
2007        .map(|exts| {
2008            exts.iter()
2009                .filter(|ext| !matches!(ext, Extension::AppDataDictionary(_)))
2010                .cloned()
2011                .collect::<Vec<_>>()
2012        })
2013        .unwrap_or_default();
2014    let new_extensions: Vec<Extension> = other_extensions
2015        .into_iter()
2016        .chain(std::iter::once(vc_extension))
2017        .collect();
2018    Extensions::from_vec(new_extensions)
2019        .map_err(|_| crate::error::LibraryError::custom("Failed to build VC leaf-node extensions"))
2020}
2021
2022#[cfg(test)]
2023mod tests {
2024    use super::*;
2025    use openmls_rust_crypto::{MemoryStorage, OpenMlsRustCrypto};
2026    use openmls_traits::{
2027        random::OpenMlsRand,
2028        storage::{StorageProvider, CURRENT_VERSION},
2029        OpenMlsProvider,
2030    };
2031
2032    const CIPHERSUITE: Ciphersuite = Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519;
2033
2034    /// Register a full `VcDerivationEpochState` and a matching
2035    /// `OperationSecretTree` for a fresh epoch, returning the derived
2036    /// `EpochId` and the leaf index it was registered with.
2037    fn register_epoch_state(provider: &OpenMlsRustCrypto, leaf_index: LeafNodeIndex) -> EpochId {
2038        use crate::components::vc_operation_tree::OperationSecretTree;
2039
2040        let emulator = EmulatorEpochSecret::new(
2041            &provider
2042                .rand()
2043                .random_vec(CIPHERSUITE.hash_length())
2044                .expect("randomness"),
2045        );
2046        let epoch_id = emulator
2047            .derive_epoch_id(provider.crypto(), CIPHERSUITE)
2048            .expect("derive epoch id");
2049        let epoch_encryption_key = emulator
2050            .derive_epoch_encryption_key(provider.crypto(), CIPHERSUITE)
2051            .expect("derive epoch encryption key");
2052        let reuse_guard_secret = emulator
2053            .derive_reuse_guard_secret(provider.crypto(), CIPHERSUITE)
2054            .expect("derive reuse guard secret");
2055        let generation_id_secret = emulator
2056            .derive_generation_id_secret(provider.crypto(), CIPHERSUITE)
2057            .expect("derive generation id secret");
2058        let epoch_base_secret = emulator
2059            .derive_epoch_base_secret(provider.crypto(), CIPHERSUITE)
2060            .expect("derive epoch base secret");
2061        let emulation_group_size = TreeSize::new(2);
2062        let state = VcDerivationEpochState::new(
2063            leaf_index,
2064            epoch_encryption_key,
2065            reuse_guard_secret,
2066            generation_id_secret,
2067            emulation_group_size,
2068            CIPHERSUITE,
2069        );
2070        <MemoryStorage as StorageProvider<CURRENT_VERSION>>::write_vc_derivation_epoch_state(
2071            provider.storage(),
2072            &epoch_id,
2073            &state,
2074        )
2075        .expect("write derivation epoch state");
2076        let operation_tree = OperationSecretTree::new(epoch_base_secret, emulation_group_size);
2077        <MemoryStorage as StorageProvider<CURRENT_VERSION>>::write_vc_operation_tree(
2078            provider.storage(),
2079            &epoch_id,
2080            &operation_tree,
2081        )
2082        .expect("write operation tree");
2083        epoch_id
2084    }
2085
2086    /// The assembly helper fills `leaf_index` from the registered
2087    /// `VcDerivationEpochState` for the epoch.
2088    #[test]
2089    fn assemble_upload_reads_leaf_index_from_state() {
2090        let provider = OpenMlsRustCrypto::default();
2091        let leaf_index = LeafNodeIndex::new(5);
2092        let epoch_id = register_epoch_state(&provider, leaf_index);
2093        let infos = vec![
2094            KeyPackageInfo {
2095                key_package_ref: KeyPackageRef::from_slice(b"kp-ref-a"),
2096                cipher_suite: CIPHERSUITE,
2097                key_package_index: 0,
2098            },
2099            KeyPackageInfo {
2100                key_package_ref: KeyPackageRef::from_slice(b"kp-ref-b"),
2101                cipher_suite: CIPHERSUITE,
2102                key_package_index: 1,
2103            },
2104        ];
2105
2106        let upload = assemble_vc_key_package_upload(provider.storage(), epoch_id.clone(), 4, infos)
2107            .expect("assemble upload");
2108
2109        assert_eq!(upload.epoch_id, epoch_id);
2110        assert_eq!(upload.leaf_index, leaf_index);
2111        assert_eq!(upload.generation, 4);
2112        assert_eq!(upload.key_package_info.len(), 2);
2113    }
2114
2115    /// Assembling for an unregistered epoch fails with
2116    /// `MissingDerivationEpochState`.
2117    #[test]
2118    fn assemble_upload_without_state_fails() {
2119        let provider = OpenMlsRustCrypto::default();
2120        let epoch_id = EpochId(b"unregistered-epoch".to_vec().into());
2121        let err = assemble_vc_key_package_upload(provider.storage(), epoch_id, 0, Vec::new())
2122            .expect_err("assemble must fail without registered state");
2123        assert_eq!(err, VirtualClientsError::MissingDerivationEpochState);
2124    }
2125
2126    /// `process_vc_key_package_upload` stores one material entry per info,
2127    /// readable back via `retained_key_package_material` keyed by the
2128    /// KeyPackage reference, each carrying its own batch index.
2129    #[test]
2130    fn process_upload_stores_records() {
2131        let provider = OpenMlsRustCrypto::default();
2132        let leaf_index = LeafNodeIndex::new(0);
2133        let epoch_id = register_epoch_state(&provider, leaf_index);
2134        let ref_a = KeyPackageRef::from_slice(b"kp-ref-a");
2135        let ref_b = KeyPackageRef::from_slice(b"kp-ref-b");
2136        let upload = KeyPackageUpload {
2137            epoch_id: epoch_id.clone(),
2138            leaf_index,
2139            generation: 0,
2140            key_package_info: vec![
2141                KeyPackageInfo {
2142                    key_package_ref: ref_a.clone(),
2143                    cipher_suite: CIPHERSUITE,
2144                    key_package_index: 0,
2145                },
2146                KeyPackageInfo {
2147                    key_package_ref: ref_b.clone(),
2148                    cipher_suite: CIPHERSUITE,
2149                    key_package_index: 1,
2150                },
2151            ],
2152        };
2153
2154        process_vc_key_package_upload(&provider, &upload).expect("process upload");
2155
2156        let material_a: RetainedKeyPackageMaterial = <MemoryStorage as StorageProvider<
2157            CURRENT_VERSION,
2158        >>::retained_key_package_material(
2159            provider.storage(), &ref_a
2160        )
2161        .expect("read material a")
2162        .expect("material a present");
2163        assert_eq!(material_a.epoch_id, epoch_id);
2164        assert_eq!(material_a.leaf_index, leaf_index);
2165        assert_eq!(material_a.generation, 0);
2166        assert_eq!(material_a.key_package_index, 0);
2167        assert_eq!(material_a.key_package_ciphersuite, CIPHERSUITE);
2168
2169        let material_b: RetainedKeyPackageMaterial = <MemoryStorage as StorageProvider<
2170            CURRENT_VERSION,
2171        >>::retained_key_package_material(
2172            provider.storage(), &ref_b
2173        )
2174        .expect("read material b")
2175        .expect("material b present");
2176        assert_eq!(material_b.epoch_id, epoch_id);
2177        assert_eq!(material_b.leaf_index, leaf_index);
2178        assert_eq!(material_b.generation, 0);
2179        assert_eq!(material_b.key_package_index, 1);
2180        assert_eq!(material_b.key_package_ciphersuite, CIPHERSUITE);
2181    }
2182
2183    /// `delete_key_package` removes the associated retained VC material.
2184    #[test]
2185    fn delete_key_package_removes_vc_record() {
2186        let provider = OpenMlsRustCrypto::default();
2187        let leaf_index = LeafNodeIndex::new(0);
2188        let epoch_id = register_epoch_state(&provider, leaf_index);
2189        let kp_ref = KeyPackageRef::from_slice(b"kp-ref");
2190        let upload = KeyPackageUpload {
2191            epoch_id,
2192            leaf_index,
2193            generation: 0,
2194            key_package_info: vec![KeyPackageInfo {
2195                key_package_ref: kp_ref.clone(),
2196                cipher_suite: CIPHERSUITE,
2197                key_package_index: 0,
2198            }],
2199        };
2200        process_vc_key_package_upload(&provider, &upload).expect("process upload");
2201
2202        let present: Option<RetainedKeyPackageMaterial> = <MemoryStorage as StorageProvider<
2203            CURRENT_VERSION,
2204        >>::retained_key_package_material(
2205            provider.storage(), &kp_ref
2206        )
2207        .expect("read material");
2208        assert!(present.is_some());
2209
2210        <MemoryStorage as StorageProvider<CURRENT_VERSION>>::delete_key_package(
2211            provider.storage(),
2212            &kp_ref,
2213        )
2214        .expect("delete key package");
2215
2216        let after: Option<RetainedKeyPackageMaterial> = <MemoryStorage as StorageProvider<
2217            CURRENT_VERSION,
2218        >>::retained_key_package_material(
2219            provider.storage(), &kp_ref
2220        )
2221        .expect("read material after delete");
2222        assert!(after.is_none());
2223    }
2224
2225    fn setup_key_and_epoch_id(provider: &OpenMlsRustCrypto) -> (EpochEncryptionKey, EpochId) {
2226        let emulator = EmulatorEpochSecret::new(
2227            &provider
2228                .rand()
2229                .random_vec(CIPHERSUITE.hash_length())
2230                .expect("randomness"),
2231        );
2232        let key = emulator
2233            .derive_epoch_encryption_key(provider.crypto(), CIPHERSUITE)
2234            .expect("derive ek");
2235        let epoch_id = emulator
2236            .derive_epoch_id(provider.crypto(), CIPHERSUITE)
2237            .expect("derive epoch id");
2238        (key, epoch_id)
2239    }
2240
2241    /// Round-trip both `DerivationInfoTbe` variants through `encrypt` and
2242    /// `decrypt`. Catches any disagreement between the two methods on the
2243    /// derived key/nonce, the AAD, or the tagless TLS layout of the
2244    /// plaintext, and confirms each variant decodes only under its own
2245    /// operation type.
2246    #[test]
2247    fn derivation_info_tbe_roundtrip() {
2248        let provider = OpenMlsRustCrypto::default();
2249        let (key, epoch_id) = setup_key_and_epoch_id(&provider);
2250        let leaf_encryption_key = provider.rand().random_vec(32).expect("randomness");
2251
2252        let key_package_tbe = DerivationInfoTbe::KeyPackage {
2253            leaf_index: LeafNodeIndex::new(7),
2254            generation: 3,
2255            key_package_index: 5,
2256        };
2257        let leaf_node_tbe = DerivationInfoTbe::LeafNode {
2258            leaf_index: LeafNodeIndex::new(7),
2259            generation: 3,
2260            external_init_secret: None,
2261        };
2262        let external_commit_tbe = DerivationInfoTbe::LeafNode {
2263            leaf_index: LeafNodeIndex::new(7),
2264            generation: 3,
2265            external_init_secret: Some(ExternalInitSecret::from_slice(b"external init secret")),
2266        };
2267
2268        // The key_package form carries the trailing key_package_index (u32),
2269        // while the leaf_node (commit) form carries an absent
2270        // optional<ExternalInitSecret> (one presence octet).
2271        let key_package_bytes = key_package_tbe
2272            .tls_serialize_detached()
2273            .expect("serialize key package tbe");
2274        let leaf_node_bytes = leaf_node_tbe
2275            .tls_serialize_detached()
2276            .expect("serialize leaf node tbe");
2277        assert_eq!(key_package_bytes.len(), leaf_node_bytes.len() + 3);
2278
2279        for (original, operation_type) in [
2280            (key_package_tbe, VirtualClientOperationType::KeyPackage),
2281            (leaf_node_tbe, VirtualClientOperationType::LeafNode),
2282            (external_commit_tbe, VirtualClientOperationType::LeafNode),
2283        ] {
2284            let derivation_info = DerivationInfo::encrypt(
2285                provider.crypto(),
2286                CIPHERSUITE,
2287                &key,
2288                epoch_id.clone(),
2289                &leaf_encryption_key,
2290                &original,
2291            )
2292            .expect("encrypt");
2293            assert_eq!(derivation_info.epoch_id(), &epoch_id);
2294            let decrypted = derivation_info
2295                .decrypt(
2296                    provider.crypto(),
2297                    CIPHERSUITE,
2298                    &key,
2299                    &leaf_encryption_key,
2300                    operation_type,
2301                )
2302                .expect("decrypt");
2303            assert_eq!(original, decrypted);
2304        }
2305    }
2306
2307    /// Pin the serialized `DerivationInfoTBE` layout to the spec's select,
2308    /// byte for byte: `uint32 leaf_index`, `uint32 generation`, then the
2309    /// `key_package_index` (key_package case) or the
2310    /// `optional<ExternalInitSecret>` (commit case) with nothing trailing.
2311    /// Catches conventions drift that the roundtrip test cannot see.
2312    #[test]
2313    fn derivation_info_tbe_wire_format_matches_spec() {
2314        let absent = DerivationInfoTbe::LeafNode {
2315            leaf_index: LeafNodeIndex::new(7),
2316            generation: 3,
2317            external_init_secret: None,
2318        }
2319        .tls_serialize_detached()
2320        .expect("serialize");
2321        assert_eq!(
2322            absent,
2323            [0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x03, 0x00]
2324        );
2325
2326        let present = DerivationInfoTbe::LeafNode {
2327            leaf_index: LeafNodeIndex::new(7),
2328            generation: 3,
2329            external_init_secret: Some(ExternalInitSecret::from_slice(b"init")),
2330        }
2331        .tls_serialize_detached()
2332        .expect("serialize");
2333        assert_eq!(
2334            present,
2335            [0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x03, 0x01, 0x04, b'i', b'n', b'i', b't']
2336        );
2337
2338        let key_package = DerivationInfoTbe::KeyPackage {
2339            leaf_index: LeafNodeIndex::new(7),
2340            generation: 3,
2341            key_package_index: 5,
2342        }
2343        .tls_serialize_detached()
2344        .expect("serialize");
2345        assert_eq!(
2346            key_package,
2347            [0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x05]
2348        );
2349    }
2350
2351    /// The TBE plaintext must be consumed exactly. A trailing octet, which is
2352    /// what a peer implementing the superseded draft revision with its
2353    /// trailing `optional<GroupCreationSecret>` would produce, is rejected
2354    /// for both variants.
2355    #[test]
2356    fn derivation_info_tbe_rejects_trailing_data() {
2357        let variants = [
2358            (
2359                DerivationInfoTbe::LeafNode {
2360                    leaf_index: LeafNodeIndex::new(7),
2361                    generation: 3,
2362                    external_init_secret: None,
2363                },
2364                VirtualClientOperationType::LeafNode,
2365            ),
2366            (
2367                DerivationInfoTbe::KeyPackage {
2368                    leaf_index: LeafNodeIndex::new(7),
2369                    generation: 3,
2370                    key_package_index: 5,
2371                },
2372                VirtualClientOperationType::KeyPackage,
2373            ),
2374        ];
2375        for (tbe, operation_type) in variants {
2376            let mut bytes = tbe.tls_serialize_detached().expect("serialize");
2377            bytes.push(0x00);
2378            let result = DerivationInfoTbe::deserialize_for_operation(&bytes, operation_type);
2379            assert_eq!(result, Err(VirtualClientsError::DerivationInfoMalformed));
2380        }
2381    }
2382
2383    /// Debug output of the TBE must not leak the carried init secret.
2384    #[test]
2385    fn external_init_secret_debug_is_redacted() {
2386        let tbe = DerivationInfoTbe::LeafNode {
2387            leaf_index: LeafNodeIndex::new(7),
2388            generation: 3,
2389            external_init_secret: Some(ExternalInitSecret::from_slice(b"very secret bytes")),
2390        };
2391        let debug = format!("{tbe:?}");
2392        assert!(debug.contains("<redacted>"));
2393        assert!(!debug.contains("secret bytes"));
2394        assert!(!debug.to_lowercase().contains("76657279"));
2395    }
2396
2397    /// Decryption must fail when the leaf encryption key used as the
2398    /// key/nonce derivation context does not match the one used for
2399    /// encryption. This is what binds the derivation info to the leaf
2400    /// that carries it.
2401    #[test]
2402    fn decryption_fails_with_wrong_leaf_encryption_key() {
2403        let provider = OpenMlsRustCrypto::default();
2404        let (key, epoch_id) = setup_key_and_epoch_id(&provider);
2405        let leaf_encryption_key = provider.rand().random_vec(32).expect("randomness");
2406        let tbe = DerivationInfoTbe::LeafNode {
2407            leaf_index: LeafNodeIndex::new(1),
2408            generation: 0,
2409            external_init_secret: None,
2410        };
2411        let derivation_info = DerivationInfo::encrypt(
2412            provider.crypto(),
2413            CIPHERSUITE,
2414            &key,
2415            epoch_id,
2416            &leaf_encryption_key,
2417            &tbe,
2418        )
2419        .expect("encrypt");
2420        let other_leaf_encryption_key = provider.rand().random_vec(32).expect("randomness");
2421        let err = derivation_info
2422            .decrypt(
2423                provider.crypto(),
2424                CIPHERSUITE,
2425                &key,
2426                &other_leaf_encryption_key,
2427                VirtualClientOperationType::LeafNode,
2428            )
2429            .expect_err("decryption with the wrong context must fail");
2430        assert_eq!(err, VirtualClientsError::DerivationInfoDecryptionFailed);
2431    }
2432
2433    /// The per-KeyPackage seed secret is deterministic for a given index,
2434    /// distinct across indices, and the init and encryption keys derived from
2435    /// one seed are separated from each other.
2436    #[test]
2437    fn key_package_seed_derivation_is_indexed_and_label_separated() {
2438        let provider = OpenMlsRustCrypto::default();
2439        let operation_secret = OperationSecret::from(Secret::from_slice(
2440            &provider
2441                .rand()
2442                .random_vec(CIPHERSUITE.hash_length())
2443                .expect("randomness"),
2444        ));
2445
2446        let seed_zero = operation_secret
2447            .derive_key_package_seed_secret(provider.crypto(), CIPHERSUITE, 0)
2448            .expect("derive seed 0");
2449        let seed_zero_again = operation_secret
2450            .derive_key_package_seed_secret(provider.crypto(), CIPHERSUITE, 0)
2451            .expect("derive seed 0 again");
2452        let seed_one = operation_secret
2453            .derive_key_package_seed_secret(provider.crypto(), CIPHERSUITE, 1)
2454            .expect("derive seed 1");
2455
2456        let init_zero = seed_zero
2457            .derive_init_key_secret(provider.crypto(), CIPHERSUITE)
2458            .expect("derive init key 0")
2459            .generate_init_key_pair(provider.crypto(), CIPHERSUITE)
2460            .expect("generate init pair 0");
2461        let init_zero_again = seed_zero_again
2462            .derive_init_key_secret(provider.crypto(), CIPHERSUITE)
2463            .expect("derive init key 0 again")
2464            .generate_init_key_pair(provider.crypto(), CIPHERSUITE)
2465            .expect("generate init pair 0 again");
2466        let init_one = seed_one
2467            .derive_init_key_secret(provider.crypto(), CIPHERSUITE)
2468            .expect("derive init key 1")
2469            .generate_init_key_pair(provider.crypto(), CIPHERSUITE)
2470            .expect("generate init pair 1");
2471
2472        // Same index derives deterministically.
2473        assert_eq!(init_zero.public, init_zero_again.public);
2474        // Different indices derive distinct seeds, hence distinct init keys.
2475        assert_ne!(init_zero.public, init_one.public);
2476
2477        // Init and encryption keys from one seed are label-separated.
2478        let encryption_zero = seed_zero
2479            .derive_encryption_key_secret(provider.crypto(), CIPHERSUITE)
2480            .expect("derive encryption key 0")
2481            .generate_encryption_key_pair(provider.crypto(), CIPHERSUITE)
2482            .expect("generate encryption pair 0");
2483        assert_ne!(
2484            init_zero.public.as_slice(),
2485            encryption_zero.public_key().as_slice()
2486        );
2487    }
2488
2489    /// The per-KeyPackage seed is imported into the target ciphersuite: the
2490    /// same operation secret and index yield different seeds for different
2491    /// target ciphersuites, because the target ciphersuite is bound into the
2492    /// `KeyPackageSeedContext` and the import runs under the target's KDF.
2493    #[test]
2494    fn key_package_seed_binds_target_ciphersuite() {
2495        let provider = OpenMlsRustCrypto::default();
2496        let operation_secret = OperationSecret::from(Secret::from_slice(
2497            &provider
2498                .rand()
2499                .random_vec(CIPHERSUITE.hash_length())
2500                .expect("randomness"),
2501        ));
2502        // Same KDF hash (SHA-256) as `CIPHERSUITE`, so the two seeds have
2503        // equal length and differ only through the ciphersuite binding.
2504        let other_ciphersuite = Ciphersuite::MLS_128_DHKEMX25519_CHACHA20POLY1305_SHA256_Ed25519;
2505
2506        let seed = operation_secret
2507            .derive_key_package_seed_secret(provider.crypto(), CIPHERSUITE, 0)
2508            .expect("derive seed");
2509        let seed_other_suite = operation_secret
2510            .derive_key_package_seed_secret(provider.crypto(), other_ciphersuite, 0)
2511            .expect("derive seed under other target ciphersuite");
2512
2513        assert_ne!(seed.0.as_slice(), seed_other_suite.0.as_slice());
2514    }
2515
2516    /// The `target_operation_secret` of a `leaf_node` operation is
2517    /// deterministic and binds both the target ciphersuite and the
2518    /// higher-level group's id; the encryption and path-generation secrets
2519    /// derived from it are label-separated.
2520    #[test]
2521    fn target_operation_secret_binds_ciphersuite_and_group_id() {
2522        let provider = OpenMlsRustCrypto::default();
2523        let operation_secret = OperationSecret::from(Secret::from_slice(
2524            &provider
2525                .rand()
2526                .random_vec(CIPHERSUITE.hash_length())
2527                .expect("randomness"),
2528        ));
2529        let group_id = GroupId::from_slice(b"group-a");
2530        let other_ciphersuite = Ciphersuite::MLS_128_DHKEMX25519_CHACHA20POLY1305_SHA256_Ed25519;
2531
2532        let target = operation_secret
2533            .derive_target_operation_secret(provider.crypto(), CIPHERSUITE, &group_id)
2534            .expect("derive target operation secret");
2535        let target_again = operation_secret
2536            .derive_target_operation_secret(provider.crypto(), CIPHERSUITE, &group_id)
2537            .expect("derive target operation secret again");
2538        let target_other_group = operation_secret
2539            .derive_target_operation_secret(
2540                provider.crypto(),
2541                CIPHERSUITE,
2542                &GroupId::from_slice(b"group-b"),
2543            )
2544            .expect("derive target operation secret for other group");
2545        let target_other_suite = operation_secret
2546            .derive_target_operation_secret(provider.crypto(), other_ciphersuite, &group_id)
2547            .expect("derive target operation secret under other target ciphersuite");
2548
2549        // Same inputs derive deterministically.
2550        assert_eq!(target.0.as_slice(), target_again.0.as_slice());
2551        // A different group id or a different target ciphersuite derives a
2552        // distinct secret.
2553        assert_ne!(target.0.as_slice(), target_other_group.0.as_slice());
2554        assert_ne!(target.0.as_slice(), target_other_suite.0.as_slice());
2555
2556        // Encryption and path-generation secrets from one target operation
2557        // secret are label-separated.
2558        let encryption_key_secret = target
2559            .derive_encryption_key_secret(provider.crypto(), CIPHERSUITE)
2560            .expect("derive encryption key secret");
2561        let path_generation_secret = target
2562            .derive_path_generation_secret(provider.crypto(), CIPHERSUITE)
2563            .expect("derive path generation secret");
2564        assert_ne!(
2565            encryption_key_secret.0.as_slice(),
2566            path_generation_secret.0.as_slice()
2567        );
2568    }
2569
2570    /// The group-creation epoch secret is deterministic for a given seed,
2571    /// distinct across seeds, and label-separated from the encryption key
2572    /// secret derived from the same seed.
2573    #[test]
2574    fn group_creation_secret_derivation_is_deterministic_and_label_separated() {
2575        let provider = OpenMlsRustCrypto::default();
2576        let operation_secret = OperationSecret::from(Secret::from_slice(
2577            &provider
2578                .rand()
2579                .random_vec(CIPHERSUITE.hash_length())
2580                .expect("randomness"),
2581        ));
2582
2583        let seed_zero = operation_secret
2584            .derive_key_package_seed_secret(provider.crypto(), CIPHERSUITE, 0)
2585            .expect("derive seed 0");
2586        let seed_one = operation_secret
2587            .derive_key_package_seed_secret(provider.crypto(), CIPHERSUITE, 1)
2588            .expect("derive seed 1");
2589
2590        let epoch_secret_zero = seed_zero
2591            .derive_group_creation_secret(provider.crypto(), CIPHERSUITE)
2592            .expect("derive group creation secret 0");
2593        let epoch_secret_zero_again = seed_zero
2594            .derive_group_creation_secret(provider.crypto(), CIPHERSUITE)
2595            .expect("derive group creation secret 0 again");
2596        let epoch_secret_one = seed_one
2597            .derive_group_creation_secret(provider.crypto(), CIPHERSUITE)
2598            .expect("derive group creation secret 1");
2599
2600        // Same seed derives deterministically.
2601        assert_eq!(
2602            epoch_secret_zero.as_slice(),
2603            epoch_secret_zero_again.as_slice()
2604        );
2605        // Different seeds derive distinct epoch secrets.
2606        assert_ne!(epoch_secret_zero.as_slice(), epoch_secret_one.as_slice());
2607
2608        // The epoch secret is label-separated from the encryption key secret
2609        // derived from the same seed.
2610        let encryption_key_secret = seed_zero
2611            .derive_encryption_key_secret(provider.crypto(), CIPHERSUITE)
2612            .expect("derive encryption key 0");
2613        assert_ne!(
2614            epoch_secret_zero.as_slice(),
2615            encryption_key_secret.0.as_slice()
2616        );
2617    }
2618
2619    /// A repeated `key_package_index` is rejected with
2620    /// `DuplicateKeyPackageIndex` carrying the offending index.
2621    #[test]
2622    fn validate_rejects_duplicate_index() {
2623        let infos = vec![
2624            KeyPackageInfo {
2625                key_package_ref: KeyPackageRef::from_slice(b"kp-ref-a"),
2626                cipher_suite: CIPHERSUITE,
2627                key_package_index: 2,
2628            },
2629            KeyPackageInfo {
2630                key_package_ref: KeyPackageRef::from_slice(b"kp-ref-b"),
2631                cipher_suite: CIPHERSUITE,
2632                key_package_index: 2,
2633            },
2634        ];
2635        let err = validate_key_package_infos(&infos).expect_err("duplicate index must be rejected");
2636        assert_eq!(err, VirtualClientsError::DuplicateKeyPackageIndex(2));
2637    }
2638
2639    /// A repeated `KeyPackageRef` is rejected with `DuplicateKeyPackageRef`.
2640    #[test]
2641    fn validate_rejects_duplicate_ref() {
2642        let infos = vec![
2643            KeyPackageInfo {
2644                key_package_ref: KeyPackageRef::from_slice(b"kp-ref-a"),
2645                cipher_suite: CIPHERSUITE,
2646                key_package_index: 0,
2647            },
2648            KeyPackageInfo {
2649                key_package_ref: KeyPackageRef::from_slice(b"kp-ref-a"),
2650                cipher_suite: CIPHERSUITE,
2651                key_package_index: 1,
2652            },
2653        ];
2654        let err = validate_key_package_infos(&infos).expect_err("duplicate ref must be rejected");
2655        assert_eq!(err, VirtualClientsError::DuplicateKeyPackageRef);
2656    }
2657
2658    /// A batch with distinct indices and references passes validation.
2659    #[test]
2660    fn validate_accepts_distinct_infos() {
2661        let infos = vec![
2662            KeyPackageInfo {
2663                key_package_ref: KeyPackageRef::from_slice(b"kp-ref-a"),
2664                cipher_suite: CIPHERSUITE,
2665                key_package_index: 0,
2666            },
2667            KeyPackageInfo {
2668                key_package_ref: KeyPackageRef::from_slice(b"kp-ref-b"),
2669                cipher_suite: CIPHERSUITE,
2670                key_package_index: 1,
2671            },
2672        ];
2673        validate_key_package_infos(&infos).expect("distinct infos must pass");
2674    }
2675
2676    /// A malformed upload is rejected before the batch generation is consumed,
2677    /// so a later valid upload reusing the same generation still succeeds and
2678    /// stores its retained material.
2679    #[test]
2680    fn process_upload_rejects_malformed_without_consuming_generation() {
2681        let provider = OpenMlsRustCrypto::default();
2682        let leaf_index = LeafNodeIndex::new(0);
2683        let epoch_id = register_epoch_state(&provider, leaf_index);
2684        let ref_a = KeyPackageRef::from_slice(b"kp-ref-a");
2685        let ref_b = KeyPackageRef::from_slice(b"kp-ref-b");
2686
2687        let malformed = KeyPackageUpload {
2688            epoch_id: epoch_id.clone(),
2689            leaf_index,
2690            generation: 0,
2691            key_package_info: vec![
2692                KeyPackageInfo {
2693                    key_package_ref: ref_a.clone(),
2694                    cipher_suite: CIPHERSUITE,
2695                    key_package_index: 0,
2696                },
2697                KeyPackageInfo {
2698                    key_package_ref: ref_b.clone(),
2699                    cipher_suite: CIPHERSUITE,
2700                    key_package_index: 0,
2701                },
2702            ],
2703        };
2704        let err = process_vc_key_package_upload(&provider, &malformed)
2705            .expect_err("malformed upload must be rejected");
2706        assert_eq!(err, VirtualClientsError::DuplicateKeyPackageIndex(0));
2707
2708        let valid = KeyPackageUpload {
2709            epoch_id: epoch_id.clone(),
2710            leaf_index,
2711            generation: 0,
2712            key_package_info: vec![
2713                KeyPackageInfo {
2714                    key_package_ref: ref_a.clone(),
2715                    cipher_suite: CIPHERSUITE,
2716                    key_package_index: 0,
2717                },
2718                KeyPackageInfo {
2719                    key_package_ref: ref_b.clone(),
2720                    cipher_suite: CIPHERSUITE,
2721                    key_package_index: 1,
2722                },
2723            ],
2724        };
2725        process_vc_key_package_upload(&provider, &valid)
2726            .expect("valid upload reusing the same generation must succeed");
2727
2728        let material_a: RetainedKeyPackageMaterial = <MemoryStorage as StorageProvider<
2729            CURRENT_VERSION,
2730        >>::retained_key_package_material(
2731            provider.storage(), &ref_a
2732        )
2733        .expect("read material a")
2734        .expect("material a present");
2735        assert_eq!(material_a.epoch_id, epoch_id);
2736        assert_eq!(material_a.generation, 0);
2737        assert_eq!(material_a.key_package_index, 0);
2738
2739        let material_b: RetainedKeyPackageMaterial = <MemoryStorage as StorageProvider<
2740            CURRENT_VERSION,
2741        >>::retained_key_package_material(
2742            provider.storage(), &ref_b
2743        )
2744        .expect("read material b")
2745        .expect("material b present");
2746        assert_eq!(material_b.key_package_index, 1);
2747    }
2748
2749    /// Build a `VcDerivationEpochState` from raw emulator-epoch-secret
2750    /// bytes, so two siblings sharing the same bytes can be compared.
2751    fn state_from_secret_bytes(
2752        provider: &OpenMlsRustCrypto,
2753        secret_bytes: &[u8],
2754        leaf_index: LeafNodeIndex,
2755    ) -> VcDerivationEpochState {
2756        let emulator = EmulatorEpochSecret::new(secret_bytes);
2757        let epoch_encryption_key = emulator
2758            .derive_epoch_encryption_key(provider.crypto(), CIPHERSUITE)
2759            .expect("derive epoch encryption key");
2760        let reuse_guard_secret = emulator
2761            .derive_reuse_guard_secret(provider.crypto(), CIPHERSUITE)
2762            .expect("derive reuse guard secret");
2763        let generation_id_secret = emulator
2764            .derive_generation_id_secret(provider.crypto(), CIPHERSUITE)
2765            .expect("derive generation id secret");
2766        VcDerivationEpochState::new(
2767            leaf_index,
2768            epoch_encryption_key,
2769            reuse_guard_secret,
2770            generation_id_secret,
2771            TreeSize::new(2),
2772            CIPHERSUITE,
2773        )
2774    }
2775
2776    /// The generation ID is deterministic for fixed inputs, changes when any
2777    /// `PrivateMessageContext` field changes, and two siblings that share the
2778    /// same emulator epoch secret derive the same value (so a DS can compare
2779    /// them for equality across siblings).
2780    #[test]
2781    fn generation_id_is_deterministic_and_context_sensitive() {
2782        let provider = OpenMlsRustCrypto::default();
2783        let secret_bytes = provider
2784            .rand()
2785            .random_vec(CIPHERSUITE.hash_length())
2786            .expect("randomness");
2787        let state = state_from_secret_bytes(&provider, &secret_bytes, LeafNodeIndex::new(0));
2788
2789        let group_id = GroupId::from_slice(b"higher-level-group");
2790        let epoch = GroupEpoch::from(7);
2791        let derive = |group_id: &GroupId, epoch, generation, ratchet_type| {
2792            state
2793                .derive_generation_id(provider.crypto(), group_id, epoch, generation, ratchet_type)
2794                .expect("derive generation id")
2795        };
2796
2797        let base = derive(&group_id, epoch, 3, RatchetType::Application);
2798        // The generation ID is `Kdf.Nh` bytes long.
2799        assert_eq!(base.as_slice().len(), CIPHERSUITE.hash_length());
2800        // Deterministic for fixed inputs.
2801        assert_eq!(base, derive(&group_id, epoch, 3, RatchetType::Application));
2802        // Sensitive to the generation, the epoch, the group id, and the
2803        // ratchet type.
2804        assert_ne!(base, derive(&group_id, epoch, 4, RatchetType::Application));
2805        assert_ne!(
2806            base,
2807            derive(&group_id, GroupEpoch::from(8), 3, RatchetType::Application)
2808        );
2809        assert_ne!(
2810            base,
2811            derive(
2812                &GroupId::from_slice(b"other-group"),
2813                epoch,
2814                3,
2815                RatchetType::Application
2816            )
2817        );
2818        assert_ne!(base, derive(&group_id, epoch, 3, RatchetType::Handshake));
2819
2820        // A sibling sharing the same emulator epoch secret derives the same
2821        // generation ID, even from a different leaf index: the leaf index is
2822        // not part of the PrivateMessageContext.
2823        let sibling = state_from_secret_bytes(&provider, &secret_bytes, LeafNodeIndex::new(5));
2824        let sibling_id = sibling
2825            .derive_generation_id(
2826                provider.crypto(),
2827                &group_id,
2828                epoch,
2829                3,
2830                RatchetType::Application,
2831            )
2832            .expect("sibling derive generation id");
2833        assert_eq!(base, sibling_id);
2834    }
2835
2836    #[test]
2837    #[allow(deprecated)]
2838    fn legacy_registration_record_layout_is_frozen() {
2839        let record = RegisteredVcDerivationEpoch {
2840            group_epoch: GroupEpoch::from(7),
2841            epoch_id: EpochId::new(vec![1, 2, 3]),
2842        };
2843        let json = serde_json::to_string(&record).expect("serialize legacy record");
2844        assert_eq!(json, r#"{"group_epoch":7,"epoch_id":[1,2,3]}"#);
2845        let decoded: RegisteredVcDerivationEpoch =
2846            serde_json::from_str(&json).expect("deserialize legacy record");
2847        assert_eq!(decoded, record);
2848    }
2849
2850    #[test]
2851    #[allow(deprecated)]
2852    fn legacy_bindings_record_layout_is_frozen() {
2853        let record = VcEmulationBindings {
2854            bindings: VecDeque::from([
2855                (GroupEpoch::from(7), EpochId::new(vec![1, 2, 3])),
2856                (GroupEpoch::from(8), EpochId::new(vec![4, 5, 6])),
2857            ]),
2858        };
2859        let json = serde_json::to_string(&record).expect("serialize legacy record");
2860        assert_eq!(json, r#"{"bindings":[[7,[1,2,3]],[8,[4,5,6]]]}"#);
2861        let decoded: VcEmulationBindings =
2862            serde_json::from_str(&json).expect("deserialize legacy record");
2863        assert_eq!(decoded, record);
2864
2865        let entries = decoded.into_entries();
2866        assert_eq!(
2867            entries,
2868            vec![
2869                (GroupEpoch::from(7), EpochId::new(vec![1, 2, 3])),
2870                (GroupEpoch::from(8), EpochId::new(vec![4, 5, 6])),
2871            ]
2872        );
2873    }
2874
2875    #[test]
2876    fn log_entry_layout_is_frozen() {
2877        let entry = VcDerivationEpochLogEntry {
2878            sequence: 2,
2879            group_epoch: GroupEpoch::from(7),
2880            epoch_id: EpochId::new(vec![1, 2, 3]),
2881            registered_at: SystemTime::UNIX_EPOCH + std::time::Duration::new(1_700_000_000, 42),
2882        };
2883        let json = serde_json::to_string(&entry).expect("serialize log entry");
2884        assert_eq!(
2885            json,
2886            r#"{"sequence":2,"group_epoch":7,"epoch_id":[1,2,3],"registered_at":{"secs_since_epoch":1700000000,"nanos_since_epoch":42}}"#
2887        );
2888        let decoded: VcDerivationEpochLogEntry =
2889            serde_json::from_str(&json).expect("deserialize log entry");
2890        assert_eq!(decoded, entry);
2891    }
2892
2893    #[test]
2894    fn binding_layout_is_frozen() {
2895        let binding = VcEmulationBinding {
2896            group_epoch: GroupEpoch::from(7),
2897            epoch_id: EpochId::new(vec![1, 2, 3]),
2898        };
2899        let json = serde_json::to_string(&binding).expect("serialize binding");
2900        assert_eq!(json, r#"{"group_epoch":7,"epoch_id":[1,2,3]}"#);
2901        let decoded: VcEmulationBinding = serde_json::from_str(&json).expect("deserialize binding");
2902        assert_eq!(decoded, binding);
2903    }
2904
2905    #[test]
2906    fn binding_from_legacy_record() {
2907        let epoch_id = EpochId::new(vec![4, 5, 6]);
2908        let binding = VcEmulationBinding::from_legacy_record(GroupEpoch::from(3), epoch_id.clone());
2909        assert_eq!(binding.group_epoch, GroupEpoch::from(3));
2910        assert_eq!(binding.epoch_id(), &epoch_id);
2911    }
2912
2913    #[test]
2914    fn log_entry_from_legacy_record() {
2915        let epoch_id = EpochId::new(vec![4, 5, 6]);
2916        let registered_at = SystemTime::UNIX_EPOCH;
2917        let entry = VcDerivationEpochLogEntry::from_legacy_record(
2918            GroupEpoch::from(3),
2919            epoch_id.clone(),
2920            registered_at,
2921        );
2922
2923        assert_eq!(entry.sequence, 0);
2924        assert_eq!(entry.group_epoch, GroupEpoch::from(3));
2925        assert_eq!(entry.epoch_id(), &epoch_id);
2926        assert_eq!(entry.registered_at, registered_at);
2927
2928        // A log holding only the converted entry treats it as the newest one,
2929        // so neither pruning path drops it.
2930        let mut log = VcDerivationEpochLog {
2931            entries: VecDeque::from([entry]),
2932        };
2933        assert!(log.shrink_to(1).is_empty());
2934        assert!(log.drop_superseded_before(SystemTime::now()).is_empty());
2935        assert_eq!(
2936            log.newest().map(|entry| entry.epoch_id.clone()),
2937            Some(epoch_id)
2938        );
2939    }
2940
2941    fn log_entry(
2942        group_epoch: u64,
2943        epoch_id: &EpochId,
2944        registered_at: SystemTime,
2945    ) -> VcDerivationEpochLogEntry {
2946        VcDerivationEpochLogEntry {
2947            sequence: group_epoch,
2948            group_epoch: GroupEpoch::from(group_epoch),
2949            epoch_id: epoch_id.clone(),
2950            registered_at,
2951        }
2952    }
2953
2954    #[test]
2955    fn log_reconstruction_orders_by_sequence() {
2956        let provider = OpenMlsRustCrypto::default();
2957        let group_id = GroupId::from_slice(b"emulation-group");
2958        let first = log_entry(0, &EpochId::new(vec![1]), SystemTime::UNIX_EPOCH);
2959        let second = log_entry(1, &EpochId::new(vec![2]), SystemTime::UNIX_EPOCH);
2960        let third = log_entry(2, &EpochId::new(vec![3]), SystemTime::UNIX_EPOCH);
2961        // Written out of order. The provider returns entries unordered anyway,
2962        // so the sort must come from the sequence numbers alone.
2963        for entry in [&second, &third, &first] {
2964            <MemoryStorage as StorageProvider<CURRENT_VERSION>>::write_vc_derivation_epoch_log_entry(
2965                provider.storage(),
2966                &group_id,
2967                &entry.epoch_id,
2968                entry,
2969            )
2970            .expect("write log entry");
2971        }
2972
2973        let log = VcDerivationEpochLog::load(provider.storage(), &group_id).expect("load the log");
2974        assert_eq!(
2975            log.newest().map(|entry| entry.epoch_id.clone()),
2976            Some(third.epoch_id.clone())
2977        );
2978        assert_eq!(
2979            newest_vc_derivation_epoch(provider.storage(), &group_id).expect("newest epoch"),
2980            Some(third.epoch_id.clone())
2981        );
2982
2983        // Pruning drops the entries with the lowest sequences first.
2984        let mut log = log;
2985        assert_eq!(log.shrink_to(2), vec![first.epoch_id.clone()]);
2986        assert_eq!(
2987            log.newest().map(|entry| entry.epoch_id.clone()),
2988            Some(third.epoch_id)
2989        );
2990    }
2991
2992    #[test]
2993    fn wall_clock_sweep_measures_from_supersession() {
2994        let old = EpochId::new(vec![1]);
2995        let mid = EpochId::new(vec![2]);
2996        let new = EpochId::new(vec![3]);
2997        let start = SystemTime::UNIX_EPOCH;
2998        let minutes = |m: u64| std::time::Duration::from_secs(m * 60);
2999        // `old` lives from `start` until `mid` supersedes it 10 minutes before
3000        // the 24 h mark, where `new` in turn supersedes `mid`.
3001        let mut log = VcDerivationEpochLog {
3002            entries: VecDeque::from([
3003                log_entry(0, &old, start),
3004                log_entry(1, &mid, start + minutes(23 * 60 + 50)),
3005                log_entry(2, &new, start + minutes(24 * 60)),
3006            ]),
3007        };
3008
3009        // A 24 h sweep 5 minutes past the day puts the cutoff well after
3010        // `old`'s registration, but `old` was only just superseded and stays.
3011        assert!(log.drop_superseded_before(start + minutes(5)).is_empty());
3012        // Once the cutoff passes `old`'s supersession, `old` goes. `mid` was
3013        // superseded later and stays.
3014        assert_eq!(
3015            log.drop_superseded_before(start + minutes(23 * 60 + 55)),
3016            vec![old]
3017        );
3018        // The newest entry has no successor and survives any cutoff.
3019        assert_eq!(
3020            log.drop_superseded_before(start + minutes(48 * 60)),
3021            vec![mid]
3022        );
3023        assert_eq!(log.newest().map(|entry| entry.epoch_id.clone()), Some(new));
3024    }
3025}