Skip to main content

openmls/components/
vc_operation_tree.rs

1//! Virtual Client Operation Secret Tree (mls-virtual-clients draft).
2//!
3//! A tree of secrets with the same structure as the RFC 9420 secret tree
4//! (Section 9): it has the same set of nodes and edges as the emulation
5//! group's ratchet tree at the corresponding epoch, parent-to-child node
6//! derivation uses the same `"tree"` label with `"left"` / `"right"`
7//! context, and each leaf is expanded once it is first used. It differs
8//! from the RFC 9420 secret tree in two ways: the root is the
9//! per-emulation-epoch `epoch_base_secret` rather than a secret derived
10//! from `encryption_secret`, and each leaf expands into one operation
11//! ratchet per `VirtualClientOperationType` instead of a handshake and an
12//! application sender ratchet.
13//!
14//! Each ratchet hands out one `OperationSecret` per generation, bound to
15//! the spec's `OperationContext`
16//! `(epoch_id, leaf_index, generation, operation_type, operation_context)`.
17//!
18//! Forward secrecy mirrors the RFC 9420 secret tree: parent node secrets
19//! are deleted once their children are derived, a leaf secret is deleted as
20//! soon as the initial ratchet secrets for all operation types have been
21//! derived, and ratchet heads plus generation secrets are deleted as soon
22//! as the operation secret of a generation has been derived. Deriving for a
23//! generation ahead of the ratchet head retains only the
24//! context-independent `operation_generation_secret` of each skipped
25//! generation, since the final operation secret also binds the (then still
26//! unknown) operation context. The retained entry is deleted when the
27//! operation for that generation arrives. Retention is bounded by
28//! `MAXIMUM_FORWARD_DISTANCE` and `OUT_OF_ORDER_TOLERANCE`.
29
30use std::collections::BTreeMap;
31
32use openmls_traits::{crypto::OpenMlsCrypto, types::Ciphersuite};
33use serde::{Deserialize, Serialize};
34use tls_codec::{Serialize as _, TlsSerialize, TlsSize, VLByteVec};
35
36use crate::{
37    binary_tree::{
38        array_representation::{
39            direct_path, left, right, root, ParentNodeIndex, TreeNodeIndex, TreeSize,
40        },
41        LeafNodeIndex,
42    },
43    ciphersuite::Secret,
44    components::vc_derivation_info::{
45        EpochId, OperationSecret, VirtualClientOperationType, VirtualClientsError,
46    },
47    tree::secret_tree::derive_child_secrets,
48    utils::vector_converter,
49};
50
51/// `ExpandWithLabel` label for the initial operation ratchet secret of each
52/// operation type, expanded from a leaf secret.
53const OPERATION_RATCHET_INIT_LABEL: &str = "vc operation init";
54/// `DeriveSecret` label for the per-generation `operation_generation_secret`.
55const OPERATION_GENERATION_LABEL: &str = "VC Operation Secret";
56/// `DeriveSecret` label for advancing an operation ratchet to the next
57/// generation.
58const OPERATION_RATCHET_ADVANCE_LABEL: &str = "VC Operation Ratchet";
59/// `ExpandWithLabel` label for the final operation secret, expanded from an
60/// `operation_generation_secret` with the TLS-serialized [`OperationContext`].
61const OPERATION_SECRET_LABEL: &str = "vc operation";
62
63/// How far beyond the current ratchet head a requested generation may lie.
64/// Requests further out fail with
65/// [`VirtualClientsError::OperationGenerationTooDistant`], which also stops a
66/// malicious sibling from forcing up to `u32::MAX` KDF steps through a
67/// fabricated `generation` in a commit's `DerivationInfo`.
68///
69/// Operation ratchets advance once per virtual-client operation rather than
70/// once per message, so legitimate gaps stay small: commits within one
71/// higher-level group are DS-ordered, but one tree spans all higher-level
72/// groups of the virtual client, so operations from different groups can be
73/// processed out of allocation order, and DS-rejected commits leave burned
74/// generations that receivers skip. This bound and
75/// [`OUT_OF_ORDER_TOLERANCE`] can become configurable later alongside the
76/// planned emulation-group configuration.
77const MAXIMUM_FORWARD_DISTANCE: u32 = 1024;
78
79/// How many skipped `operation_generation_secret`s a ratchet retains.
80/// Skipping past more than this many unconsumed generations evicts the oldest
81/// retained entries first, after which they fail with
82/// [`VirtualClientsError::OperationGenerationConsumed`]. The draft says
83/// implementations SHOULD bound how many skipped generations they retain,
84/// since every retained secret weakens forward secrecy within the emulation
85/// epoch. See [`MAXIMUM_FORWARD_DISTANCE`] for why legitimate gaps stay
86/// small and for the plan to make both bounds configurable.
87const OUT_OF_ORDER_TOLERANCE: usize = 32;
88
89/// Per-emulation-epoch Virtual Client Operation Secret Tree.
90///
91/// Rooted at the epoch's `epoch_base_secret` and shaped like the emulation
92/// group's ratchet tree at the corresponding epoch (sized by the leaf
93/// count, including blank leaves). Node secrets and per-leaf operation
94/// ratchets are derived lazily on first use, and consumed material is deleted
95/// as it is used (see the module documentation for the forward-secrecy
96/// rules).
97///
98/// # Concurrency
99///
100/// One tree is shared by all higher-level groups the virtual client is a
101/// member of. Every derivation mutates it, so a load-derive-store cycle
102/// against the storage provider must be atomic per emulation epoch.
103/// Applications that process messages for multiple higher-level groups in
104/// parallel must serialize these cycles. Two concurrent cycles on separate
105/// copies of the tree can allocate the same generation for two different
106/// operations (the key reuse the draft forbids) and last-write-wins
107/// persistence loses the other copy's punctured nodes and retained skipped
108/// generations.
109#[derive(Debug, Serialize, Deserialize)]
110pub struct OperationSecretTree {
111    leaf_nodes: Vec<Option<Secret>>,
112    parent_nodes: Vec<Option<Secret>>,
113    operation_ratchets: Vec<Option<LeafOperationRatchets>>,
114    size: TreeSize,
115}
116
117impl OperationSecretTree {
118    /// Create a tree rooted at `epoch_base_secret` with the given `size`.
119    /// The inner node secrets and the operation ratchets only get derived
120    /// when operation secrets are requested.
121    ///
122    /// `Secret` and `TreeSize` are crate-internal, so unlike the derivation
123    /// methods this constructor cannot be `pub`.
124    pub(crate) fn new(epoch_base_secret: Secret, size: TreeSize) -> Self {
125        let leaf_count = size.leaf_count() as usize;
126        let mut tree = Self {
127            leaf_nodes: std::iter::repeat_with(|| None).take(leaf_count).collect(),
128            parent_nodes: std::iter::repeat_with(|| None).take(leaf_count).collect(),
129            operation_ratchets: std::iter::repeat_with(|| None).take(leaf_count).collect(),
130            size,
131        };
132        // Set the epoch base secret in the root node. We ignore the Result
133        // here, since we rely on the tree math to be correct, i.e.
134        // root(size) < size.
135        let _ = tree.set_node(root(size), Some(epoch_base_secret));
136        tree
137    }
138
139    /// Derive the operation secret for the given coordinates, advancing the
140    /// per-leaf, per-operation-type ratchet as necessary.
141    ///
142    /// Deriving for a generation ahead of the ratchet head advances the head
143    /// past it while retaining the context-independent generation secrets of
144    /// the skipped generations (at most [`OUT_OF_ORDER_TOLERANCE`], oldest
145    /// evicted first), so the corresponding operations can still be processed
146    /// when they arrive. Asking for a generation whose operation secret was
147    /// already derived or evicted fails with
148    /// [`VirtualClientsError::OperationGenerationConsumed`], a generation
149    /// more than [`MAXIMUM_FORWARD_DISTANCE`] beyond the head fails with
150    /// [`VirtualClientsError::OperationGenerationTooDistant`], and a leaf
151    /// index outside the tree fails with
152    /// [`VirtualClientsError::IndexOutOfBounds`].
153    ///
154    /// # Warning
155    ///
156    /// This mutates the tree: it advances and punctures ratchets. The caller
157    /// MUST persist the mutated tree before any other cycle reads it, as a
158    /// single atomic load-derive-store per emulation epoch. Deriving against
159    /// a stale copy, or not persisting before the next derive, re-serves a
160    /// consumed generation and reuses key material, the reuse the draft
161    /// forbids. See the type-level `# Concurrency` note.
162    #[allow(clippy::too_many_arguments)]
163    pub(crate) fn derive_operation_secret(
164        &mut self,
165        crypto: &impl OpenMlsCrypto,
166        ciphersuite: Ciphersuite,
167        epoch_id: &EpochId,
168        leaf_index: LeafNodeIndex,
169        operation_type: VirtualClientOperationType,
170        generation: u32,
171        operation_context: &[u8],
172    ) -> Result<OperationSecret, VirtualClientsError> {
173        let ratchet = self.ratchet_mut(crypto, ciphersuite, leaf_index, operation_type)?;
174        let operation_generation_secret =
175            ratchet.generation_secret(crypto, ciphersuite, generation)?;
176        let context = OperationContext {
177            epoch_id: epoch_id.clone(),
178            leaf_index,
179            generation,
180            operation_type,
181            operation_context: operation_context.to_vec().into(),
182        };
183        // `operation_generation_secret` is dropped when this function
184        // returns, deleting it as required by the spec.
185        context.expand_operation_secret(crypto, ciphersuite, &operation_generation_secret)
186    }
187
188    /// Advance the caller's own ratchet for `operation_type` at
189    /// `own_leaf_index` and return the generation it was at together with the
190    /// operation secret for that generation. Use this when sending an
191    /// operation. Receivers re-derive the same secret positionally via
192    /// [`OperationSecretTree::derive_operation_secret`].
193    ///
194    /// # Warning
195    ///
196    /// This mutates the tree: it advances the own ratchet by one generation.
197    /// The caller MUST persist the mutated tree in the same atomic
198    /// load-derive-store cycle, before the allocated generation can be
199    /// observed on the wire. Allocating from a stale copy, or persisting
200    /// late, lets two operations share a generation and reuse key material,
201    /// the reuse the draft forbids. See the type-level `# Concurrency` note.
202    pub(crate) fn next_operation_secret(
203        &mut self,
204        crypto: &impl OpenMlsCrypto,
205        ciphersuite: Ciphersuite,
206        epoch_id: &EpochId,
207        own_leaf_index: LeafNodeIndex,
208        operation_type: VirtualClientOperationType,
209        operation_context: &[u8],
210    ) -> Result<(u32, OperationSecret), VirtualClientsError> {
211        let generation = self
212            .ratchet_mut(crypto, ciphersuite, own_leaf_index, operation_type)?
213            .head_generation();
214        let operation_secret = self.derive_operation_secret(
215            crypto,
216            ciphersuite,
217            epoch_id,
218            own_leaf_index,
219            operation_type,
220            generation,
221            operation_context,
222        )?;
223        Ok((generation, operation_secret))
224    }
225
226    /// Return the ratchet for `(leaf_index, operation_type)`, initializing
227    /// the leaf's operation ratchets first if necessary.
228    fn ratchet_mut(
229        &mut self,
230        crypto: &impl OpenMlsCrypto,
231        ciphersuite: Ciphersuite,
232        leaf_index: LeafNodeIndex,
233        operation_type: VirtualClientOperationType,
234    ) -> Result<&mut OperationRatchet, VirtualClientsError> {
235        if leaf_index.u32() >= self.size.leaf_count() {
236            log::error!("vc: leaf index is larger than the operation secret tree size.");
237            return Err(VirtualClientsError::IndexOutOfBounds);
238        }
239        if self
240            .operation_ratchets
241            .get(leaf_index.usize())
242            .ok_or(VirtualClientsError::IndexOutOfBounds)?
243            .is_none()
244        {
245            self.initialize_leaf_ratchets(crypto, ciphersuite, leaf_index)?;
246        }
247        let ratchets = self
248            .operation_ratchets
249            .get_mut(leaf_index.usize())
250            .and_then(|ratchets| ratchets.as_mut())
251            // We just initialized the ratchets, so this should not happen.
252            .ok_or(VirtualClientsError::LibraryError)?;
253        Ok(ratchets.ratchet_mut(operation_type))
254    }
255
256    /// Derive the node secrets down to `leaf_index`, expand the leaf secret
257    /// into one initial ratchet secret per operation type and delete it.
258    fn initialize_leaf_ratchets(
259        &mut self,
260        crypto: &impl OpenMlsCrypto,
261        ciphersuite: Ciphersuite,
262        leaf_index: LeafNodeIndex,
263    ) -> Result<(), VirtualClientsError> {
264        // If we don't have a secret in the leaf node, we derive it from the
265        // closest populated ancestor.
266        if self.get_node(leaf_index.into())?.is_none() {
267            // Collect empty nodes in the direct path until a non-empty node
268            // is found.
269            let mut empty_nodes: Vec<ParentNodeIndex> = Vec::new();
270            for parent_node in direct_path(leaf_index, self.size) {
271                empty_nodes.push(parent_node);
272                if self.get_node(parent_node.into())?.is_some() {
273                    break;
274                }
275            }
276            // Derive the secrets down all the way to the leaf node, deleting
277            // each parent secret once its children are populated.
278            empty_nodes.reverse();
279            for parent_node in empty_nodes {
280                self.derive_down(crypto, ciphersuite, parent_node)?;
281            }
282        }
283
284        // Take the leaf secret out of the tree: the spec requires deleting
285        // it as soon as the initial ratchet secrets for all operation types
286        // have been derived. `initialize` consumes and drops it.
287        let leaf_secret = self
288            .leaf_nodes
289            .get_mut(leaf_index.usize())
290            .ok_or(VirtualClientsError::IndexOutOfBounds)?
291            .take()
292            // We just derived all necessary nodes, so this should not happen.
293            .ok_or(VirtualClientsError::LibraryError)?;
294        let ratchets = LeafOperationRatchets::initialize(crypto, ciphersuite, leaf_secret)?;
295        *self
296            .operation_ratchets
297            .get_mut(leaf_index.usize())
298            .ok_or(VirtualClientsError::IndexOutOfBounds)? = Some(ratchets);
299        Ok(())
300    }
301
302    /// Derive the secrets for the child nodes of a parent node, deleting the
303    /// parent node's secret.
304    fn derive_down(
305        &mut self,
306        crypto: &impl OpenMlsCrypto,
307        ciphersuite: Ciphersuite,
308        parent_index: ParentNodeIndex,
309    ) -> Result<(), VirtualClientsError> {
310        let parent_secret = self
311            .parent_nodes
312            .get_mut(parent_index.usize())
313            .ok_or(VirtualClientsError::IndexOutOfBounds)?
314            // Taking the secret deletes the parent node.
315            .take()
316            // This function only gets called top to bottom, so this should
317            // not happen.
318            .ok_or(VirtualClientsError::LibraryError)?;
319        let (left_secret, right_secret) =
320            derive_child_secrets(&parent_secret, crypto, ciphersuite)?;
321        self.set_node(left(parent_index), Some(left_secret))?;
322        self.set_node(right(parent_index), Some(right_secret))?;
323        Ok(())
324    }
325
326    fn get_node(&self, index: TreeNodeIndex) -> Result<Option<&Secret>, VirtualClientsError> {
327        match index {
328            TreeNodeIndex::Leaf(leaf_index) => Ok(self
329                .leaf_nodes
330                .get(leaf_index.usize())
331                .ok_or(VirtualClientsError::IndexOutOfBounds)?
332                .as_ref()),
333            TreeNodeIndex::Parent(parent_index) => Ok(self
334                .parent_nodes
335                .get(parent_index.usize())
336                .ok_or(VirtualClientsError::IndexOutOfBounds)?
337                .as_ref()),
338        }
339    }
340
341    fn set_node(
342        &mut self,
343        index: TreeNodeIndex,
344        secret: Option<Secret>,
345    ) -> Result<(), VirtualClientsError> {
346        match index {
347            TreeNodeIndex::Leaf(leaf_index) => {
348                *self
349                    .leaf_nodes
350                    .get_mut(leaf_index.usize())
351                    .ok_or(VirtualClientsError::IndexOutOfBounds)? = secret;
352            }
353            TreeNodeIndex::Parent(parent_index) => {
354                *self
355                    .parent_nodes
356                    .get_mut(parent_index.usize())
357                    .ok_or(VirtualClientsError::IndexOutOfBounds)? = secret;
358            }
359        }
360        Ok(())
361    }
362}
363
364/// The per-operation-type ratchets expanded from one leaf secret, one per
365/// non-reserved [`VirtualClientOperationType`].
366#[derive(Debug, Serialize, Deserialize)]
367struct LeafOperationRatchets {
368    key_package: OperationRatchet,
369    leaf_node: OperationRatchet,
370    application: OperationRatchet,
371}
372
373impl LeafOperationRatchets {
374    /// Expand `leaf_secret` into the initial ratchet secret for every
375    /// non-reserved operation type:
376    ///
377    /// ```text
378    /// operation_ratchet_secret[operation_type][0] =
379    ///   ExpandWithLabel(leaf_secret, "vc operation init", operation_type, Kdf.Nh)
380    /// ```
381    ///
382    /// where `operation_type` is the TLS-encoded
383    /// [`VirtualClientOperationType`] value. `leaf_secret` is consumed and
384    /// dropped here, per the spec requirement to delete it as soon as the
385    /// initial ratchet secrets for all operation types have been derived.
386    fn initialize(
387        crypto: &impl OpenMlsCrypto,
388        ciphersuite: Ciphersuite,
389        leaf_secret: Secret,
390    ) -> Result<Self, VirtualClientsError> {
391        let initial_ratchet_secret =
392            |operation_type: VirtualClientOperationType| -> Result<Secret, VirtualClientsError> {
393                let context = operation_type.tls_serialize_detached()?;
394                Ok(leaf_secret.kdf_expand_label(
395                    crypto,
396                    ciphersuite,
397                    OPERATION_RATCHET_INIT_LABEL,
398                    &context,
399                    ciphersuite.hash_length(),
400                )?)
401            };
402        Ok(Self {
403            key_package: OperationRatchet::new(initial_ratchet_secret(
404                VirtualClientOperationType::KeyPackage,
405            )?),
406            leaf_node: OperationRatchet::new(initial_ratchet_secret(
407                VirtualClientOperationType::LeafNode,
408            )?),
409            application: OperationRatchet::new(initial_ratchet_secret(
410                VirtualClientOperationType::Application,
411            )?),
412        })
413    }
414
415    fn ratchet_mut(&mut self, operation_type: VirtualClientOperationType) -> &mut OperationRatchet {
416        match operation_type {
417            VirtualClientOperationType::KeyPackage => &mut self.key_package,
418            VirtualClientOperationType::LeafNode => &mut self.leaf_node,
419            VirtualClientOperationType::Application => &mut self.application,
420        }
421    }
422}
423
424/// A single operation ratchet: the current ratchet head plus the
425/// context-independent `operation_generation_secret`s retained for
426/// generations that were skipped over.
427#[derive(Debug, Serialize, Deserialize)]
428struct OperationRatchet {
429    /// The `operation_ratchet_secret` for `next_generation`.
430    ratchet_secret: Secret,
431    next_generation: u32,
432    /// Retained `operation_generation_secret`s of skipped generations,
433    /// deleted when the operation for the generation arrives. A generation
434    /// below `next_generation` without an entry here was already consumed.
435    #[serde(with = "vector_converter")]
436    retained_generation_secrets: BTreeMap<u32, Secret>,
437}
438
439impl OperationRatchet {
440    fn new(initial_ratchet_secret: Secret) -> Self {
441        Self {
442            ratchet_secret: initial_ratchet_secret,
443            next_generation: 0,
444            retained_generation_secrets: BTreeMap::new(),
445        }
446    }
447
448    /// The generation the ratchet head is currently at, i.e. the generation
449    /// the next own operation would use.
450    fn head_generation(&self) -> u32 {
451        self.next_generation
452    }
453
454    /// Return the `operation_generation_secret` for `generation`, advancing
455    /// the ratchet head past it if necessary. Skipped generations retain
456    /// their generation secret, keeping at most [`OUT_OF_ORDER_TOLERANCE`]
457    /// entries by evicting the oldest first. Asking for a generation that was
458    /// already consumed or evicted fails with
459    /// [`VirtualClientsError::OperationGenerationConsumed`], and a generation
460    /// more than [`MAXIMUM_FORWARD_DISTANCE`] beyond the head fails with
461    /// [`VirtualClientsError::OperationGenerationTooDistant`] without
462    /// advancing the head.
463    fn generation_secret(
464        &mut self,
465        crypto: &impl OpenMlsCrypto,
466        ciphersuite: Ciphersuite,
467        generation: u32,
468    ) -> Result<Secret, VirtualClientsError> {
469        if generation < self.next_generation {
470            // Removing the entry deletes the retained secret once the caller
471            // drops it.
472            return self
473                .retained_generation_secrets
474                .remove(&generation)
475                .ok_or(VirtualClientsError::OperationGenerationConsumed);
476        }
477        if self.next_generation < u32::MAX - MAXIMUM_FORWARD_DISTANCE
478            && generation > self.next_generation + MAXIMUM_FORWARD_DISTANCE
479        {
480            log::error!(
481                "vc: requested operation generation {generation} is more than \
482                 {MAXIMUM_FORWARD_DISTANCE} beyond the ratchet head {}.",
483                self.next_generation
484            );
485            return Err(VirtualClientsError::OperationGenerationTooDistant);
486        }
487        while self.next_generation < generation {
488            let skipped_generation = self.next_generation;
489            let skipped_secret = self.advance(crypto, ciphersuite)?;
490            self.retained_generation_secrets
491                .insert(skipped_generation, skipped_secret);
492        }
493        // Evict the oldest retained entries first, dropping their secrets.
494        while self.retained_generation_secrets.len() > OUT_OF_ORDER_TOLERANCE {
495            self.retained_generation_secrets.pop_first();
496        }
497        self.advance(crypto, ciphersuite)
498    }
499
500    /// One ratchet step:
501    ///
502    /// ```text
503    /// operation_generation_secret =
504    ///   DeriveSecret(operation_ratchet_secret, "VC Operation Secret")
505    /// next_operation_ratchet_secret =
506    ///   DeriveSecret(operation_ratchet_secret, "VC Operation Ratchet")
507    /// ```
508    ///
509    /// Returns the `operation_generation_secret` for the head generation and
510    /// advances the head, deleting the consumed `operation_ratchet_secret`.
511    fn advance(
512        &mut self,
513        crypto: &impl OpenMlsCrypto,
514        ciphersuite: Ciphersuite,
515    ) -> Result<Secret, VirtualClientsError> {
516        if self.next_generation == u32::MAX {
517            return Err(VirtualClientsError::OperationRatchetTooLong);
518        }
519        let operation_generation_secret =
520            self.ratchet_secret
521                .derive_secret(crypto, ciphersuite, OPERATION_GENERATION_LABEL)?;
522        // Overwriting the head deletes the consumed ratchet secret.
523        self.ratchet_secret = self.ratchet_secret.derive_secret(
524            crypto,
525            ciphersuite,
526            OPERATION_RATCHET_ADVANCE_LABEL,
527        )?;
528        self.next_generation += 1;
529        Ok(operation_generation_secret)
530    }
531}
532
533/// Context bound into each operation secret (mls-virtual-clients draft
534/// `OperationContext`):
535///
536/// ```text
537/// struct {
538///   opaque epoch_id<V>;
539///   uint32 leaf_index;
540///   uint32 generation;
541///   VirtualClientOperationType operation_type;
542///   opaque operation_context<V>;
543/// } OperationContext
544/// ```
545#[derive(Debug, TlsSize, TlsSerialize)]
546struct OperationContext {
547    epoch_id: EpochId,
548    leaf_index: LeafNodeIndex,
549    generation: u32,
550    operation_type: VirtualClientOperationType,
551    operation_context: VLByteVec,
552}
553
554impl OperationContext {
555    /// Expand the final operation secret:
556    ///
557    /// ```text
558    /// operation_secret =
559    ///   ExpandWithLabel(operation_generation_secret, "vc operation",
560    ///                   OperationContext, Kdf.Nh)
561    /// ```
562    fn expand_operation_secret(
563        &self,
564        crypto: &impl OpenMlsCrypto,
565        ciphersuite: Ciphersuite,
566        operation_generation_secret: &Secret,
567    ) -> Result<OperationSecret, VirtualClientsError> {
568        let context = self.tls_serialize_detached()?;
569        let operation_secret = operation_generation_secret.kdf_expand_label(
570            crypto,
571            ciphersuite,
572            OPERATION_SECRET_LABEL,
573            &context,
574            ciphersuite.hash_length(),
575        )?;
576        Ok(OperationSecret::from(operation_secret))
577    }
578}
579
580#[cfg(test)]
581mod tests {
582    use openmls_rust_crypto::OpenMlsRustCrypto;
583    use openmls_traits::{random::OpenMlsRand, OpenMlsProvider};
584
585    use super::*;
586    use crate::components::vc_derivation_info::EmulatorEpochSecret;
587
588    const CIPHERSUITE: Ciphersuite = Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519;
589
590    /// Build two trees from the same `epoch_base_secret` (one sender-side,
591    /// one receiver-side instance), going through the real per-epoch
592    /// derivation chain.
593    fn setup(
594        leaf_count: u32,
595    ) -> (
596        OpenMlsRustCrypto,
597        EpochId,
598        OperationSecretTree,
599        OperationSecretTree,
600    ) {
601        let provider = OpenMlsRustCrypto::default();
602        let emulator = EmulatorEpochSecret::new(
603            &provider
604                .rand()
605                .random_vec(CIPHERSUITE.hash_length())
606                .expect("randomness"),
607        );
608        let epoch_id = emulator
609            .derive_epoch_id(provider.crypto(), CIPHERSUITE)
610            .expect("derive epoch id");
611        let epoch_base_secret = emulator
612            .derive_epoch_base_secret(provider.crypto(), CIPHERSUITE)
613            .expect("derive epoch base secret");
614        let size = TreeSize::from_leaf_count(leaf_count);
615        let tree_a = OperationSecretTree::new(epoch_base_secret.clone(), size);
616        let tree_b = OperationSecretTree::new(epoch_base_secret, size);
617        (provider, epoch_id, tree_a, tree_b)
618    }
619
620    /// Two instances built from the same `epoch_base_secret` and size must
621    /// agree on the operation secret for the same coordinates and context,
622    /// regardless of the order in which they derive.
623    #[test]
624    fn cross_instance_agreement() {
625        let (provider, epoch_id, mut tree_a, mut tree_b) = setup(8);
626        let secret_a = tree_a
627            .derive_operation_secret(
628                provider.crypto(),
629                CIPHERSUITE,
630                &epoch_id,
631                LeafNodeIndex::new(2),
632                VirtualClientOperationType::LeafNode,
633                3,
634                b"commit context",
635            )
636            .expect("derive on tree a");
637        // Tree b derives generations 0..=3 in order before reaching the same
638        // coordinates.
639        for generation in 0..3 {
640            tree_b
641                .derive_operation_secret(
642                    provider.crypto(),
643                    CIPHERSUITE,
644                    &epoch_id,
645                    LeafNodeIndex::new(2),
646                    VirtualClientOperationType::LeafNode,
647                    generation,
648                    b"earlier context",
649                )
650                .expect("derive earlier generation on tree b");
651        }
652        let secret_b = tree_b
653            .derive_operation_secret(
654                provider.crypto(),
655                CIPHERSUITE,
656                &epoch_id,
657                LeafNodeIndex::new(2),
658                VirtualClientOperationType::LeafNode,
659                3,
660                b"commit context",
661            )
662            .expect("derive on tree b");
663        assert_eq!(secret_a.as_slice(), secret_b.as_slice());
664    }
665
666    /// Different generations, leaves, operation types, and contexts must all
667    /// yield different operation secrets.
668    #[test]
669    fn coordinates_and_context_bind_the_secret() {
670        let (provider, epoch_id, mut tree_a, mut tree_b) = setup(8);
671        let derive = |tree: &mut OperationSecretTree,
672                      leaf: u32,
673                      operation_type: VirtualClientOperationType,
674                      generation: u32,
675                      context: &[u8]| {
676            tree.derive_operation_secret(
677                provider.crypto(),
678                CIPHERSUITE,
679                &epoch_id,
680                LeafNodeIndex::new(leaf),
681                operation_type,
682                generation,
683                context,
684            )
685            .expect("derive operation secret")
686        };
687        let leaf_node = VirtualClientOperationType::LeafNode;
688        let baseline = derive(&mut tree_a, 0, leaf_node, 0, b"ctx");
689        let other_generation = derive(&mut tree_a, 0, leaf_node, 1, b"ctx");
690        let other_leaf = derive(&mut tree_a, 1, leaf_node, 0, b"ctx");
691        let other_type = derive(
692            &mut tree_a,
693            0,
694            VirtualClientOperationType::KeyPackage,
695            0,
696            b"ctx",
697        );
698        // Same coordinates as the baseline, but a different context. Derived
699        // on the second instance because the baseline consumed generation 0.
700        let other_context = derive(&mut tree_b, 0, leaf_node, 0, b"other ctx");
701        let secrets = [
702            baseline.as_slice(),
703            other_generation.as_slice(),
704            other_leaf.as_slice(),
705            other_type.as_slice(),
706            other_context.as_slice(),
707        ];
708        for (i, secret) in secrets.iter().enumerate() {
709            for other in &secrets[i + 1..] {
710                assert_ne!(secret, other);
711            }
712        }
713    }
714
715    /// Skipping ahead retains the skipped generations: deriving generation 5
716    /// first, generations 0 through 4 each still succeed exactly once, and
717    /// re-asking for any consumed generation fails. Skipped derivations match
718    /// an instance that derives in order.
719    #[test]
720    fn out_of_order_derivation_and_consumption() {
721        let (provider, epoch_id, mut tree_a, mut tree_b) = setup(4);
722        let leaf = LeafNodeIndex::new(0);
723        let operation_type = VirtualClientOperationType::Application;
724        let context_for = |generation: u32| format!("operation {generation}").into_bytes();
725        let in_order: Vec<_> = (0..=5)
726            .map(|generation| {
727                tree_b
728                    .derive_operation_secret(
729                        provider.crypto(),
730                        CIPHERSUITE,
731                        &epoch_id,
732                        leaf,
733                        operation_type,
734                        generation,
735                        &context_for(generation),
736                    )
737                    .expect("in-order derivation")
738            })
739            .collect();
740
741        // Generation 5 first, skipping 0..=4.
742        let skipped_ahead = tree_a
743            .derive_operation_secret(
744                provider.crypto(),
745                CIPHERSUITE,
746                &epoch_id,
747                leaf,
748                operation_type,
749                5,
750                &context_for(5),
751            )
752            .expect("derive generation 5");
753        assert_eq!(skipped_ahead.as_slice(), in_order[5].as_slice());
754
755        // Generations 0 through 4 still succeed exactly once.
756        for generation in 0..5 {
757            let retained = tree_a
758                .derive_operation_secret(
759                    provider.crypto(),
760                    CIPHERSUITE,
761                    &epoch_id,
762                    leaf,
763                    operation_type,
764                    generation,
765                    &context_for(generation),
766                )
767                .expect("derive retained generation");
768            assert_eq!(
769                retained.as_slice(),
770                in_order[generation as usize].as_slice()
771            );
772        }
773
774        // Re-asking for any consumed generation fails, including generation 5.
775        for generation in 0..=5 {
776            let err = tree_a
777                .derive_operation_secret(
778                    provider.crypto(),
779                    CIPHERSUITE,
780                    &epoch_id,
781                    leaf,
782                    operation_type,
783                    generation,
784                    &context_for(generation),
785                )
786                .expect_err("consumed generation must fail");
787            assert_eq!(err, VirtualClientsError::OperationGenerationConsumed);
788        }
789
790        // A leaf index outside the tree is rejected.
791        let out_of_bounds = LeafNodeIndex::new(TreeSize::from_leaf_count(4).leaf_count());
792        let err = tree_a
793            .derive_operation_secret(
794                provider.crypto(),
795                CIPHERSUITE,
796                &epoch_id,
797                out_of_bounds,
798                operation_type,
799                0,
800                b"ctx",
801            )
802            .expect_err("out-of-bounds leaf index must fail");
803        assert_eq!(err, VirtualClientsError::IndexOutOfBounds);
804    }
805
806    /// The "next own operation" method yields sequential generations whose
807    /// secrets match what a second instance derives positionally.
808    #[test]
809    fn next_operation_secret_advances_sequentially() {
810        let (provider, epoch_id, mut tree_a, mut tree_b) = setup(4);
811        let leaf = LeafNodeIndex::new(1);
812        let operation_type = VirtualClientOperationType::KeyPackage;
813        for expected_generation in 0..3 {
814            let context = format!("key package {expected_generation}").into_bytes();
815            let (generation, own_secret) = tree_a
816                .next_operation_secret(
817                    provider.crypto(),
818                    CIPHERSUITE,
819                    &epoch_id,
820                    leaf,
821                    operation_type,
822                    &context,
823                )
824                .expect("next operation secret");
825            assert_eq!(generation, expected_generation);
826            let positional = tree_b
827                .derive_operation_secret(
828                    provider.crypto(),
829                    CIPHERSUITE,
830                    &epoch_id,
831                    leaf,
832                    operation_type,
833                    generation,
834                    &context,
835                )
836                .expect("positional derivation");
837            assert_eq!(own_secret.as_slice(), positional.as_slice());
838        }
839    }
840
841    /// A serde round-trip of a tree mid-state (some generations consumed,
842    /// some skipped) preserves behavior: the round-tripped tree still serves
843    /// retained skipped generations, still refuses consumed ones, and
844    /// continues at the right head generation.
845    #[test]
846    fn serde_roundtrip_preserves_ratchet_state() {
847        let (provider, epoch_id, mut tree_a, mut tree_b) = setup(4);
848        let leaf = LeafNodeIndex::new(2);
849        let operation_type = VirtualClientOperationType::LeafNode;
850        // Skip ahead to generation 4 (retaining 0..=3), then consume
851        // generation 1 from the retained entries.
852        tree_a
853            .derive_operation_secret(
854                provider.crypto(),
855                CIPHERSUITE,
856                &epoch_id,
857                leaf,
858                operation_type,
859                4,
860                b"four",
861            )
862            .expect("derive generation 4");
863        tree_a
864            .derive_operation_secret(
865                provider.crypto(),
866                CIPHERSUITE,
867                &epoch_id,
868                leaf,
869                operation_type,
870                1,
871                b"one",
872            )
873            .expect("derive retained generation 1");
874
875        let serialized = serde_json::to_vec(&tree_a).expect("serialize tree");
876        let mut restored: OperationSecretTree =
877            serde_json::from_slice(&serialized).expect("deserialize tree");
878
879        // A retained skipped generation is still served and agrees with a
880        // second instance.
881        let restored_secret = restored
882            .derive_operation_secret(
883                provider.crypto(),
884                CIPHERSUITE,
885                &epoch_id,
886                leaf,
887                operation_type,
888                2,
889                b"two",
890            )
891            .expect("derive retained generation after round-trip");
892        let positional = tree_b
893            .derive_operation_secret(
894                provider.crypto(),
895                CIPHERSUITE,
896                &epoch_id,
897                leaf,
898                operation_type,
899                2,
900                b"two",
901            )
902            .expect("positional derivation");
903        assert_eq!(restored_secret.as_slice(), positional.as_slice());
904
905        // Consumed generations are still refused.
906        for (generation, context) in [(1, b"one".as_slice()), (4, b"four".as_slice())] {
907            let err = restored
908                .derive_operation_secret(
909                    provider.crypto(),
910                    CIPHERSUITE,
911                    &epoch_id,
912                    leaf,
913                    operation_type,
914                    generation,
915                    context,
916                )
917                .expect_err("consumed generation must fail after round-trip");
918            assert_eq!(err, VirtualClientsError::OperationGenerationConsumed);
919        }
920
921        // The ratchet head continues right after the skipped-ahead
922        // generation.
923        let (generation, _secret) = restored
924            .next_operation_secret(
925                provider.crypto(),
926                CIPHERSUITE,
927                &epoch_id,
928                leaf,
929                operation_type,
930                b"five",
931            )
932            .expect("next operation secret after round-trip");
933        assert_eq!(generation, 5);
934    }
935
936    /// A generation more than [`MAXIMUM_FORWARD_DISTANCE`] beyond the head is
937    /// rejected without advancing the head: the next own operation still
938    /// allocates generation 0.
939    #[test]
940    fn forward_distance_bound_rejects_without_advancing() {
941        let (provider, epoch_id, mut tree_a, _tree_b) = setup(4);
942        let leaf = LeafNodeIndex::new(0);
943        let operation_type = VirtualClientOperationType::LeafNode;
944        let err = tree_a
945            .derive_operation_secret(
946                provider.crypto(),
947                CIPHERSUITE,
948                &epoch_id,
949                leaf,
950                operation_type,
951                MAXIMUM_FORWARD_DISTANCE + 1,
952                b"ctx",
953            )
954            .expect_err("generation beyond the forward distance must fail");
955        assert_eq!(err, VirtualClientsError::OperationGenerationTooDistant);
956
957        // The head is unchanged: the next in-window derivation is still
958        // generation 0.
959        let (generation, _secret) = tree_a
960            .next_operation_secret(
961                provider.crypto(),
962                CIPHERSUITE,
963                &epoch_id,
964                leaf,
965                operation_type,
966                b"ctx",
967            )
968            .expect("next operation secret after rejected request");
969        assert_eq!(generation, 0);
970    }
971
972    /// A generation exactly at the forward-distance limit succeeds.
973    #[test]
974    fn forward_distance_boundary_succeeds() {
975        let (provider, epoch_id, mut tree_a, _tree_b) = setup(4);
976        let leaf = LeafNodeIndex::new(0);
977        let operation_type = VirtualClientOperationType::LeafNode;
978        tree_a
979            .derive_operation_secret(
980                provider.crypto(),
981                CIPHERSUITE,
982                &epoch_id,
983                leaf,
984                operation_type,
985                MAXIMUM_FORWARD_DISTANCE,
986                b"ctx",
987            )
988            .expect("generation at the forward-distance limit must succeed");
989        // The head sits right behind the consumed generation.
990        let (generation, _secret) = tree_a
991            .next_operation_secret(
992                provider.crypto(),
993                CIPHERSUITE,
994                &epoch_id,
995                leaf,
996                operation_type,
997                b"ctx",
998            )
999            .expect("next operation secret after skipping to the limit");
1000        assert_eq!(generation, MAXIMUM_FORWARD_DISTANCE + 1);
1001    }
1002
1003    /// Skipping more unconsumed generations than [`OUT_OF_ORDER_TOLERANCE`]
1004    /// evicts the oldest retained entries first: the evicted generation fails
1005    /// as consumed, while the oldest generation still within the window is
1006    /// served exactly once and agrees with an in-order instance.
1007    #[test]
1008    fn skipping_beyond_tolerance_evicts_oldest() {
1009        let (provider, epoch_id, mut tree_a, mut tree_b) = setup(4);
1010        let leaf = LeafNodeIndex::new(0);
1011        let operation_type = VirtualClientOperationType::Application;
1012        let tolerance = OUT_OF_ORDER_TOLERANCE as u32;
1013        // Skipping 0..=tolerance retains one entry more than the tolerance,
1014        // evicting generation 0.
1015        let skip_to = tolerance + 1;
1016        tree_a
1017            .derive_operation_secret(
1018                provider.crypto(),
1019                CIPHERSUITE,
1020                &epoch_id,
1021                leaf,
1022                operation_type,
1023                skip_to,
1024                b"ctx",
1025            )
1026            .expect("skipping derivation");
1027
1028        // The evicted generation 0 reports as consumed.
1029        let err = tree_a
1030            .derive_operation_secret(
1031                provider.crypto(),
1032                CIPHERSUITE,
1033                &epoch_id,
1034                leaf,
1035                operation_type,
1036                0,
1037                b"ctx",
1038            )
1039            .expect_err("evicted generation must fail");
1040        assert_eq!(err, VirtualClientsError::OperationGenerationConsumed);
1041
1042        // Reference values from an instance that derives strictly in order.
1043        let mut in_order = Vec::new();
1044        for generation in 0..=tolerance {
1045            let secret = tree_b
1046                .derive_operation_secret(
1047                    provider.crypto(),
1048                    CIPHERSUITE,
1049                    &epoch_id,
1050                    leaf,
1051                    operation_type,
1052                    generation,
1053                    b"ctx",
1054                )
1055                .expect("in-order derivation");
1056            in_order.push(secret);
1057        }
1058
1059        // The oldest and newest generations within the window are still
1060        // served, agree with the in-order instance, and are served exactly
1061        // once.
1062        for generation in [1, tolerance] {
1063            let retained = tree_a
1064                .derive_operation_secret(
1065                    provider.crypto(),
1066                    CIPHERSUITE,
1067                    &epoch_id,
1068                    leaf,
1069                    operation_type,
1070                    generation,
1071                    b"ctx",
1072                )
1073                .expect("retained generation within the window");
1074            assert_eq!(
1075                retained.as_slice(),
1076                in_order[generation as usize].as_slice()
1077            );
1078            let err = tree_a
1079                .derive_operation_secret(
1080                    provider.crypto(),
1081                    CIPHERSUITE,
1082                    &epoch_id,
1083                    leaf,
1084                    operation_type,
1085                    generation,
1086                    b"ctx",
1087                )
1088                .expect_err("second request for the same generation must fail");
1089            assert_eq!(err, VirtualClientsError::OperationGenerationConsumed);
1090        }
1091    }
1092}