Skip to main content

openmls/components/
vc_commit_data.rs

1//! Virtual-clients commit data (mls-virtual-clients draft).
2//!
3//! A virtual client attaches this struct to a commit as a Safe AAD item under
4//! [`VC_COMPONENT_ID`]. It tells the sibling emulators which actions the commit
5//! performs.
6//!
7//! ```tls
8//! enum {
9//!   reserved(0),
10//!   key_package_upload(1),
11//!   new_derivation_epoch(2),
12//!   (255)
13//! } ActionType;
14//!
15//! struct {
16//!   ActionType action_type;
17//!   select (VirtualClientAction.action_type) {
18//!     case key_package_upload:
19//!       KeyPackageUpload key_package_upload;
20//!     case new_derivation_epoch:
21//!       struct{};
22//!   };
23//! } VirtualClientAction;
24//!
25//! struct {
26//!   VirtualClientAction actions<V>;
27//! } VirtualClientCommitData;
28//! ```
29//!
30//! A [`VirtualClientCommitData`] carries at most one `new_derivation_epoch`
31//! action. That rule is enforced on construction and on deserialization.
32//!
33//! [`VC_COMPONENT_ID`]: crate::components::vc_derivation_info::VC_COMPONENT_ID
34//! [`VirtualClientCommitData`]: crate::components::vc_commit_data::VirtualClientCommitData
35
36use tls_codec::{
37    DeserializeBytes as TlsDeserializeBytesTrait, Serialize as TlsSerializeTrait,
38    TlsDeserializeBytes, TlsSerialize, TlsSize,
39};
40
41use crate::{
42    components::vc_derivation_info::{KeyPackageUpload, VC_COMPONENT_ID},
43    framing::{SafeAad, SafeAadItem},
44};
45
46/// Errors that can occur when building or parsing a [`VirtualClientCommitData`].
47#[derive(thiserror::Error, Debug, PartialEq, Eq, Clone)]
48pub enum VcCommitDataError {
49    /// More than one `new_derivation_epoch` action is present.
50    #[error("VirtualClientCommitData carries more than one new_derivation_epoch action")]
51    MultipleNewDerivationEpochs,
52    /// Encoding or decoding failure.
53    #[error("codec error: {0}")]
54    Codec(String),
55}
56
57impl From<tls_codec::Error> for VcCommitDataError {
58    fn from(err: tls_codec::Error) -> Self {
59        Self::Codec(err.to_string())
60    }
61}
62
63/// One action a virtual client's commit performs.
64#[derive(Debug, PartialEq, TlsSerialize, TlsDeserializeBytes, TlsSize)]
65#[repr(u8)]
66pub enum VirtualClientAction {
67    /// The author published a batch of KeyPackages.
68    #[tls_codec(discriminant = 1)]
69    KeyPackageUpload(KeyPackageUpload),
70    /// The author asks the group to start a new derivation epoch.
71    #[tls_codec(discriminant = 2)]
72    NewDerivationEpoch,
73}
74
75/// What a virtual client tells its siblings about a commit it authored.
76#[derive(Debug, PartialEq, TlsSerialize, TlsSize)]
77pub struct VirtualClientCommitData {
78    actions: Vec<VirtualClientAction>,
79}
80
81impl VirtualClientCommitData {
82    /// Assemble the commit data.
83    ///
84    /// Returns an error if `actions` holds more than one
85    /// [`VirtualClientAction::NewDerivationEpoch`].
86    pub fn new(actions: Vec<VirtualClientAction>) -> Result<Self, VcCommitDataError> {
87        Self::validate(&actions)?;
88        Ok(Self { actions })
89    }
90
91    /// All actions the commit performs.
92    pub fn actions(&self) -> &[VirtualClientAction] {
93        &self.actions
94    }
95
96    /// Adds a [`VirtualClientAction::NewDerivationEpoch`] action unless the
97    /// commit data already carries one.
98    pub(crate) fn require_new_derivation_epoch(&mut self) {
99        if !self.creates_derivation_epoch() {
100            self.actions.push(VirtualClientAction::NewDerivationEpoch);
101        }
102    }
103
104    /// Returns true if the commit asks for a new derivation epoch.
105    pub fn creates_derivation_epoch(&self) -> bool {
106        self.actions
107            .iter()
108            .any(|action| matches!(action, VirtualClientAction::NewDerivationEpoch))
109    }
110
111    /// The KeyPackage batches the commit publishes, in wire order.
112    pub fn key_package_uploads(&self) -> impl Iterator<Item = &KeyPackageUpload> {
113        self.actions.iter().filter_map(|action| match action {
114            VirtualClientAction::KeyPackageUpload(upload) => Some(upload),
115            VirtualClientAction::NewDerivationEpoch => None,
116        })
117    }
118
119    /// Wrap this commit data in a Safe AAD item tagged with
120    /// [`VC_COMPONENT_ID`], ready to be attached to an outgoing commit.
121    pub fn to_safe_aad_item(&self) -> Result<SafeAadItem, VcCommitDataError> {
122        let data = self.tls_serialize_detached()?;
123        Ok(SafeAadItem::new(VC_COMPONENT_ID, data))
124    }
125
126    /// Parse commit data from the bytes of a Safe AAD item tagged with
127    /// [`VC_COMPONENT_ID`]. Rejects bytes left over after the struct.
128    pub fn from_safe_aad_item_data(data: &[u8]) -> Result<Self, VcCommitDataError> {
129        Ok(Self::tls_deserialize_exact_bytes(data)?)
130    }
131
132    /// Parse the commit data carried by `safe_aad` under [`VC_COMPONENT_ID`].
133    ///
134    /// Returns `Ok(None)` when `safe_aad` carries no such item.
135    pub fn from_safe_aad(safe_aad: &SafeAad) -> Result<Option<Self>, VcCommitDataError> {
136        safe_aad
137            .get(VC_COMPONENT_ID)
138            .map(Self::from_safe_aad_item_data)
139            .transpose()
140    }
141
142    fn validate(actions: &[VirtualClientAction]) -> Result<(), VcCommitDataError> {
143        let new_epoch_actions = actions
144            .iter()
145            .filter(|action| matches!(action, VirtualClientAction::NewDerivationEpoch))
146            .count();
147        if new_epoch_actions > 1 {
148            return Err(VcCommitDataError::MultipleNewDerivationEpochs);
149        }
150        Ok(())
151    }
152}
153
154impl TlsDeserializeBytesTrait for VirtualClientCommitData {
155    fn tls_deserialize_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), tls_codec::Error> {
156        let (actions, rest) = Vec::<VirtualClientAction>::tls_deserialize_bytes(bytes)?;
157        let commit_data = VirtualClientCommitData::new(actions)
158            .map_err(|err| tls_codec::Error::DecodingError(err.to_string()))?;
159        Ok((commit_data, rest))
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use openmls_traits::types::Ciphersuite;
166
167    use super::*;
168    use crate::{
169        binary_tree::LeafNodeIndex,
170        ciphersuite::hash_ref::KeyPackageRef,
171        components::vc_derivation_info::{EpochId, KeyPackageInfo},
172    };
173
174    const CIPHERSUITE: Ciphersuite = Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519;
175
176    fn epoch_id(bytes: &[u8]) -> EpochId {
177        EpochId::new(bytes.to_vec())
178    }
179
180    fn key_package_upload() -> KeyPackageUpload {
181        KeyPackageUpload {
182            epoch_id: epoch_id(b"epoch-for-upload"),
183            leaf_index: LeafNodeIndex::new(3),
184            generation: 7,
185            key_package_info: vec![
186                KeyPackageInfo {
187                    key_package_ref: KeyPackageRef::from_slice(b"first key package ref"),
188                    cipher_suite: CIPHERSUITE,
189                    key_package_index: 0,
190                },
191                KeyPackageInfo {
192                    key_package_ref: KeyPackageRef::from_slice(b"second key package ref"),
193                    cipher_suite: CIPHERSUITE,
194                    key_package_index: 1,
195                },
196            ],
197        }
198    }
199
200    fn full_commit_data() -> VirtualClientCommitData {
201        VirtualClientCommitData::new(vec![
202            VirtualClientAction::KeyPackageUpload(key_package_upload()),
203            VirtualClientAction::NewDerivationEpoch,
204        ])
205        .expect("one new_derivation_epoch action is valid")
206    }
207
208    #[test]
209    fn vc_commit_data_roundtrip() {
210        let commit_data = full_commit_data();
211
212        let bytes = commit_data.tls_serialize_detached().unwrap();
213        let parsed = VirtualClientCommitData::tls_deserialize_exact_bytes(&bytes).unwrap();
214
215        assert_eq!(parsed, commit_data);
216        assert_eq!(parsed.tls_serialize_detached().unwrap(), bytes);
217        assert!(parsed.creates_derivation_epoch());
218        assert_eq!(
219            parsed.key_package_uploads().collect::<Vec<_>>(),
220            vec![&key_package_upload()]
221        );
222    }
223
224    #[test]
225    fn vc_commit_data_new_rejects_two_new_derivation_epochs() {
226        let err = VirtualClientCommitData::new(vec![
227            VirtualClientAction::NewDerivationEpoch,
228            VirtualClientAction::NewDerivationEpoch,
229        ])
230        .unwrap_err();
231
232        assert_eq!(err, VcCommitDataError::MultipleNewDerivationEpochs);
233    }
234
235    #[test]
236    fn vc_commit_data_deserialize_rejects_two_new_derivation_epochs() {
237        // Hand-craft bytes the constructor would refuse to produce: two
238        // `new_derivation_epoch` actions.
239        let raw_bytes = vec![0x02, 0x02, 0x02];
240
241        let err = VirtualClientCommitData::tls_deserialize_exact_bytes(&raw_bytes).unwrap_err();
242        match err {
243            tls_codec::Error::DecodingError(message) => assert!(
244                message.contains("more than one new_derivation_epoch"),
245                "unexpected error message: {message}"
246            ),
247            other => panic!("unexpected error variant: {other:?}"),
248        }
249    }
250
251    #[test]
252    fn vc_action_deserialize_rejects_reserved_and_unknown_types() {
253        for discriminant in [0x00u8, 0x03, 0xff] {
254            let err = VirtualClientAction::tls_deserialize_exact_bytes(&[discriminant])
255                .expect_err("only key_package_upload and new_derivation_epoch are valid");
256            assert!(
257                matches!(err, tls_codec::Error::UnknownValue(value) if value == discriminant as u64),
258                "unexpected error variant for {discriminant}: {err:?}"
259            );
260        }
261    }
262
263    #[test]
264    fn vc_commit_data_safe_aad_item_roundtrip() {
265        let commit_data = full_commit_data();
266
267        let item = commit_data.to_safe_aad_item().unwrap();
268        assert_eq!(item.component_id(), VC_COMPONENT_ID);
269
270        let parsed = VirtualClientCommitData::from_safe_aad_item_data(item.data()).unwrap();
271        assert_eq!(parsed, commit_data);
272    }
273
274    #[test]
275    fn vc_commit_data_from_item_data_rejects_trailing_bytes() {
276        let mut data = full_commit_data()
277            .to_safe_aad_item()
278            .unwrap()
279            .data()
280            .to_vec();
281        data.push(0x00);
282
283        let err = VirtualClientCommitData::from_safe_aad_item_data(&data).unwrap_err();
284        assert_eq!(
285            err,
286            VcCommitDataError::Codec(tls_codec::Error::TrailingData.to_string())
287        );
288    }
289}