1use std::cmp::Ordering;
48
49use tls_codec::{
50 DeserializeBytes as TlsDeserializeBytesTrait, Serialize as TlsSerializeTrait,
51 TlsDeserializeBytes, TlsSerialize, TlsSize,
52};
53
54use crate::{
55 components::vc_derivation_info::{EpochId, KeyPackageUpload, VC_COMPONENT_ID},
56 framing::SafeAadItem,
57};
58
59#[derive(thiserror::Error, Debug, PartialEq, Eq, Clone)]
61pub enum VcCommitDataError {
62 #[error("duplicate epoch id in VcEpochUsage")]
64 DuplicateEpochId,
65 #[error("VcEpochUsage entries are not sorted by the serialized epoch id in increasing order")]
68 EpochsNotSortedAscending,
69 #[error("VirtualClientCommitData carries more than one new_derivation_epoch action")]
71 MultipleNewDerivationEpochs,
72 #[error("codec error: {0}")]
74 Codec(String),
75}
76
77impl From<tls_codec::Error> for VcCommitDataError {
78 fn from(err: tls_codec::Error) -> Self {
79 Self::Codec(err.to_string())
80 }
81}
82
83#[derive(Debug, PartialEq, TlsSerialize, TlsDeserializeBytes, TlsSize)]
85#[repr(u8)]
86pub enum VirtualClientAction {
87 #[tls_codec(discriminant = 1)]
89 KeyPackageUpload(KeyPackageUpload),
90 #[tls_codec(discriminant = 2)]
92 NewDerivationEpoch,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, TlsSerialize, TlsDeserializeBytes, TlsSize)]
97pub struct VcEpochReference {
98 epoch_id: EpochId,
99}
100
101impl VcEpochReference {
102 pub fn epoch_id(&self) -> &EpochId {
104 &self.epoch_id
105 }
106}
107
108impl From<EpochId> for VcEpochReference {
109 fn from(epoch_id: EpochId) -> Self {
110 Self { epoch_id }
111 }
112}
113
114impl From<VcEpochReference> for EpochId {
115 fn from(reference: VcEpochReference) -> Self {
116 reference.epoch_id
117 }
118}
119
120#[derive(Debug, Clone, PartialEq, Eq, TlsSerialize, TlsSize)]
126pub struct VcEpochUsage {
127 in_use_epochs: Vec<VcEpochReference>,
128}
129
130impl VcEpochUsage {
131 pub fn new(epoch_ids: impl IntoIterator<Item = EpochId>) -> Result<Self, VcCommitDataError> {
137 let mut keyed = epoch_ids
138 .into_iter()
139 .map(|epoch_id| Ok((epoch_id.tls_serialize_detached()?, epoch_id)))
140 .collect::<Result<Vec<_>, tls_codec::Error>>()?;
141 keyed.sort_by(|(left, _), (right, _)| left.cmp(right));
142 keyed.dedup_by(|(left, _), (right, _)| left == right);
143 Ok(Self {
144 in_use_epochs: keyed
145 .into_iter()
146 .map(|(_, epoch_id)| VcEpochReference::from(epoch_id))
147 .collect(),
148 })
149 }
150
151 pub fn empty() -> Self {
156 Self {
157 in_use_epochs: Vec::new(),
158 }
159 }
160
161 pub fn in_use_epochs(&self) -> &[VcEpochReference] {
163 &self.in_use_epochs
164 }
165
166 pub fn epoch_ids(&self) -> impl Iterator<Item = &EpochId> {
168 self.in_use_epochs.iter().map(VcEpochReference::epoch_id)
169 }
170
171 pub fn is_empty(&self) -> bool {
173 self.in_use_epochs.is_empty()
174 }
175
176 fn from_entries(entries: Vec<VcEpochReference>) -> Result<Self, VcCommitDataError> {
177 let mut previous: Option<Vec<u8>> = None;
178 for entry in &entries {
179 let current = entry.epoch_id.tls_serialize_detached()?;
180 if let Some(previous) = &previous {
181 match current.cmp(previous) {
182 Ordering::Equal => return Err(VcCommitDataError::DuplicateEpochId),
183 Ordering::Less => return Err(VcCommitDataError::EpochsNotSortedAscending),
184 Ordering::Greater => {}
185 }
186 }
187 previous = Some(current);
188 }
189 Ok(Self {
190 in_use_epochs: entries,
191 })
192 }
193}
194
195impl TlsDeserializeBytesTrait for VcEpochUsage {
196 fn tls_deserialize_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), tls_codec::Error> {
197 let (entries, rest) = Vec::<VcEpochReference>::tls_deserialize_bytes(bytes)?;
198 let usage = VcEpochUsage::from_entries(entries)
199 .map_err(|err| tls_codec::Error::DecodingError(err.to_string()))?;
200 Ok((usage, rest))
201 }
202}
203
204#[derive(Debug, PartialEq, TlsSerialize, TlsSize)]
206pub struct VirtualClientCommitData {
207 epoch_usage: Option<VcEpochUsage>,
208 actions: Vec<VirtualClientAction>,
209}
210
211impl VirtualClientCommitData {
212 pub fn new(
221 epoch_usage: Option<VcEpochUsage>,
222 actions: Vec<VirtualClientAction>,
223 ) -> Result<Self, VcCommitDataError> {
224 Self::validate(&actions)?;
225 Ok(Self {
226 epoch_usage,
227 actions,
228 })
229 }
230
231 pub fn epoch_usage(&self) -> Option<&VcEpochUsage> {
234 self.epoch_usage.as_ref()
235 }
236
237 pub fn actions(&self) -> &[VirtualClientAction] {
239 &self.actions
240 }
241
242 pub fn creates_derivation_epoch(&self) -> bool {
244 self.actions
245 .iter()
246 .any(|action| matches!(action, VirtualClientAction::NewDerivationEpoch))
247 }
248
249 pub fn key_package_uploads(&self) -> impl Iterator<Item = &KeyPackageUpload> {
251 self.actions.iter().filter_map(|action| match action {
252 VirtualClientAction::KeyPackageUpload(upload) => Some(upload),
253 VirtualClientAction::NewDerivationEpoch => None,
254 })
255 }
256
257 pub fn to_safe_aad_item(&self) -> Result<SafeAadItem, VcCommitDataError> {
260 let data = self.tls_serialize_detached()?;
261 Ok(SafeAadItem::new(VC_COMPONENT_ID, data))
262 }
263
264 pub fn from_safe_aad_item_data(data: &[u8]) -> Result<Self, VcCommitDataError> {
267 Ok(Self::tls_deserialize_exact_bytes(data)?)
268 }
269
270 fn validate(actions: &[VirtualClientAction]) -> Result<(), VcCommitDataError> {
271 let new_epoch_actions = actions
272 .iter()
273 .filter(|action| matches!(action, VirtualClientAction::NewDerivationEpoch))
274 .count();
275 if new_epoch_actions > 1 {
276 return Err(VcCommitDataError::MultipleNewDerivationEpochs);
277 }
278 Ok(())
279 }
280}
281
282impl TlsDeserializeBytesTrait for VirtualClientCommitData {
283 fn tls_deserialize_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), tls_codec::Error> {
284 let (epoch_usage, rest) = Option::<VcEpochUsage>::tls_deserialize_bytes(bytes)?;
285 let (actions, rest) = Vec::<VirtualClientAction>::tls_deserialize_bytes(rest)?;
286 let commit_data = VirtualClientCommitData::new(epoch_usage, actions)
287 .map_err(|err| tls_codec::Error::DecodingError(err.to_string()))?;
288 Ok((commit_data, rest))
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use openmls_traits::types::Ciphersuite;
295
296 use super::*;
297 use crate::{
298 binary_tree::LeafNodeIndex, ciphersuite::hash_ref::KeyPackageRef,
299 components::vc_derivation_info::KeyPackageInfo,
300 };
301
302 const CIPHERSUITE: Ciphersuite = Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519;
303
304 fn epoch_id(bytes: &[u8]) -> EpochId {
305 EpochId::new(bytes.to_vec())
306 }
307
308 fn key_package_upload() -> KeyPackageUpload {
309 KeyPackageUpload {
310 epoch_id: epoch_id(b"epoch-for-upload"),
311 leaf_index: LeafNodeIndex::new(3),
312 generation: 7,
313 key_package_info: vec![
314 KeyPackageInfo {
315 key_package_ref: KeyPackageRef::from_slice(b"first key package ref"),
316 cipher_suite: CIPHERSUITE,
317 key_package_index: 0,
318 },
319 KeyPackageInfo {
320 key_package_ref: KeyPackageRef::from_slice(b"second key package ref"),
321 cipher_suite: CIPHERSUITE,
322 key_package_index: 1,
323 },
324 ],
325 }
326 }
327
328 fn full_commit_data() -> VirtualClientCommitData {
329 let usage = VcEpochUsage::new([epoch_id(b"aaa"), epoch_id(b"bb"), epoch_id(b"cccc")])
330 .expect("epoch ids must serialize");
331 VirtualClientCommitData::new(
332 Some(usage),
333 vec![
334 VirtualClientAction::KeyPackageUpload(key_package_upload()),
335 VirtualClientAction::NewDerivationEpoch,
336 ],
337 )
338 .expect("one new_derivation_epoch action is valid")
339 }
340
341 #[test]
342 fn vc_commit_data_roundtrip() {
343 let commit_data = full_commit_data();
344
345 let bytes = commit_data.tls_serialize_detached().unwrap();
346 let parsed = VirtualClientCommitData::tls_deserialize_exact_bytes(&bytes).unwrap();
347
348 assert_eq!(parsed, commit_data);
349 assert_eq!(parsed.tls_serialize_detached().unwrap(), bytes);
350 assert!(parsed.creates_derivation_epoch());
351 assert_eq!(
352 parsed.key_package_uploads().collect::<Vec<_>>(),
353 vec![&key_package_upload()]
354 );
355 assert_eq!(
356 parsed.epoch_usage().unwrap().in_use_epochs().len(),
357 3,
358 "all three epochs are distinct"
359 );
360 }
361
362 #[test]
363 fn vc_commit_data_absent_and_empty_epoch_usage_differ() {
364 let absent = VirtualClientCommitData::new(None, Vec::new()).unwrap();
365 let empty = VirtualClientCommitData::new(Some(VcEpochUsage::empty()), Vec::new()).unwrap();
366
367 let absent_bytes = absent.tls_serialize_detached().unwrap();
368 let empty_bytes = empty.tls_serialize_detached().unwrap();
369
370 assert_eq!(absent_bytes, vec![0x00, 0x00]);
373 assert_eq!(empty_bytes, vec![0x01, 0x00, 0x00]);
374
375 let parsed_absent =
376 VirtualClientCommitData::tls_deserialize_exact_bytes(&absent_bytes).unwrap();
377 let parsed_empty =
378 VirtualClientCommitData::tls_deserialize_exact_bytes(&empty_bytes).unwrap();
379
380 assert_eq!(parsed_absent.epoch_usage(), None);
381 assert_eq!(parsed_empty.epoch_usage(), Some(&VcEpochUsage::empty()));
382 assert!(parsed_empty.epoch_usage().unwrap().is_empty());
383 }
384
385 #[test]
386 fn vc_commit_data_new_rejects_two_new_derivation_epochs() {
387 let err = VirtualClientCommitData::new(
388 None,
389 vec![
390 VirtualClientAction::NewDerivationEpoch,
391 VirtualClientAction::NewDerivationEpoch,
392 ],
393 )
394 .unwrap_err();
395
396 assert_eq!(err, VcCommitDataError::MultipleNewDerivationEpochs);
397 }
398
399 #[test]
400 fn vc_commit_data_deserialize_rejects_two_new_derivation_epochs() {
401 let raw_bytes = vec![0x00, 0x02, 0x02, 0x02];
404
405 let err = VirtualClientCommitData::tls_deserialize_exact_bytes(&raw_bytes).unwrap_err();
406 match err {
407 tls_codec::Error::DecodingError(message) => assert!(
408 message.contains("more than one new_derivation_epoch"),
409 "unexpected error message: {message}"
410 ),
411 other => panic!("unexpected error variant: {other:?}"),
412 }
413 }
414
415 #[test]
416 fn vc_epoch_usage_new_sorts_and_dedups() {
417 let usage = VcEpochUsage::new([
418 epoch_id(b"cccc"),
419 epoch_id(b"aaa"),
420 epoch_id(b"bb"),
421 epoch_id(b"aaa"),
422 ])
423 .unwrap();
424
425 let epochs: Vec<&[u8]> = usage.epoch_ids().map(EpochId::as_bytes).collect();
426 assert_eq!(epochs, vec![b"bb".as_slice(), b"aaa", b"cccc"]);
429 }
430
431 #[test]
432 fn vc_epoch_usage_deserialize_rejects_unsorted() {
433 let entries: Vec<VcEpochReference> = vec![
434 epoch_id(b"cccc").into(),
435 epoch_id(b"aaa").into(),
436 epoch_id(b"bb").into(),
437 ];
438 let raw_bytes = entries.tls_serialize_detached().unwrap();
439
440 let err = VcEpochUsage::tls_deserialize_exact_bytes(&raw_bytes).unwrap_err();
441 match err {
442 tls_codec::Error::DecodingError(message) => assert!(
443 message.contains("not sorted"),
444 "unexpected error message: {message}"
445 ),
446 other => panic!("unexpected error variant: {other:?}"),
447 }
448 }
449
450 #[test]
451 fn vc_epoch_usage_deserialize_rejects_duplicates() {
452 let entries: Vec<VcEpochReference> =
453 vec![epoch_id(b"same").into(), epoch_id(b"same").into()];
454 let raw_bytes = entries.tls_serialize_detached().unwrap();
455
456 let err = VcEpochUsage::tls_deserialize_exact_bytes(&raw_bytes).unwrap_err();
457 match err {
458 tls_codec::Error::DecodingError(message) => assert!(
459 message.contains("duplicate"),
460 "unexpected error message: {message}"
461 ),
462 other => panic!("unexpected error variant: {other:?}"),
463 }
464 }
465
466 #[test]
467 fn vc_epoch_usage_sorting_compares_serializations() {
468 let short = epoch_id(&[0xff; 63]);
471 let long = epoch_id(&[0x00; 64]);
472
473 let usage = VcEpochUsage::new([long.clone(), short.clone()]).unwrap();
474
475 let epochs: Vec<&EpochId> = usage.epoch_ids().collect();
476 assert_eq!(epochs, vec![&short, &long]);
477 }
478
479 #[test]
480 fn vc_action_deserialize_rejects_reserved_and_unknown_types() {
481 for discriminant in [0x00u8, 0x03, 0xff] {
482 let err = VirtualClientAction::tls_deserialize_exact_bytes(&[discriminant])
483 .expect_err("only key_package_upload and new_derivation_epoch are valid");
484 assert!(
485 matches!(err, tls_codec::Error::UnknownValue(value) if value == discriminant as u64),
486 "unexpected error variant for {discriminant}: {err:?}"
487 );
488 }
489 }
490
491 #[test]
492 fn vc_commit_data_safe_aad_item_roundtrip() {
493 let commit_data = full_commit_data();
494
495 let item = commit_data.to_safe_aad_item().unwrap();
496 assert_eq!(item.component_id(), VC_COMPONENT_ID);
497
498 let parsed = VirtualClientCommitData::from_safe_aad_item_data(item.data()).unwrap();
499 assert_eq!(parsed, commit_data);
500 }
501
502 #[test]
503 fn vc_commit_data_from_item_data_rejects_trailing_bytes() {
504 let mut data = full_commit_data()
505 .to_safe_aad_item()
506 .unwrap()
507 .data()
508 .to_vec();
509 data.push(0x00);
510
511 let err = VirtualClientCommitData::from_safe_aad_item_data(&data).unwrap_err();
512 assert_eq!(
513 err,
514 VcCommitDataError::Codec(tls_codec::Error::TrailingData.to_string())
515 );
516 }
517}