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