openmls/components/
vc_commit_data.rs1use 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#[derive(thiserror::Error, Debug, PartialEq, Eq, Clone)]
48pub enum VcCommitDataError {
49 #[error("VirtualClientCommitData carries more than one new_derivation_epoch action")]
51 MultipleNewDerivationEpochs,
52 #[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#[derive(Debug, PartialEq, TlsSerialize, TlsDeserializeBytes, TlsSize)]
65#[repr(u8)]
66pub enum VirtualClientAction {
67 #[tls_codec(discriminant = 1)]
69 KeyPackageUpload(KeyPackageUpload),
70 #[tls_codec(discriminant = 2)]
72 NewDerivationEpoch,
73}
74
75#[derive(Debug, PartialEq, TlsSerialize, TlsSize)]
77pub struct VirtualClientCommitData {
78 actions: Vec<VirtualClientAction>,
79}
80
81impl VirtualClientCommitData {
82 pub fn new(actions: Vec<VirtualClientAction>) -> Result<Self, VcCommitDataError> {
87 Self::validate(&actions)?;
88 Ok(Self { actions })
89 }
90
91 pub fn actions(&self) -> &[VirtualClientAction] {
93 &self.actions
94 }
95
96 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 pub fn creates_derivation_epoch(&self) -> bool {
106 self.actions
107 .iter()
108 .any(|action| matches!(action, VirtualClientAction::NewDerivationEpoch))
109 }
110
111 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 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 pub fn from_safe_aad_item_data(data: &[u8]) -> Result<Self, VcCommitDataError> {
129 Ok(Self::tls_deserialize_exact_bytes(data)?)
130 }
131
132 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 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}