1use log::info;
9use openmls_traits::{random::OpenMlsRand, types::HpkeKeyPair, OpenMlsProvider};
10use serde::{self, Deserialize, Serialize};
11use tls_codec::Serialize as TlsSerializeTrait;
12
13#[cfg(all(test, feature = "generate-kats"))]
14use crate::test_utils::write;
15use crate::{
16 ciphersuite::*,
17 extensions::Extensions,
18 group::*,
19 schedule::{errors::KsTestVectorError, CommitSecret, *},
20 test_utils::*,
21};
22
23#[derive(Serialize, Deserialize, Debug, Clone, Default)]
24struct Exporter {
25 label: String,
26 context: String,
27 length: u32,
28 secret: String,
29}
30
31#[derive(Serialize, Deserialize, Debug, Clone, Default)]
32struct Epoch {
33 tree_hash: String,
35 commit_secret: String,
36 psk_secret: String,
37 confirmed_transcript_hash: String,
38
39 group_context: String,
41 joiner_secret: String,
42 welcome_secret: String,
43 init_secret: String,
44 sender_data_secret: String,
45 encryption_secret: String,
46 exporter_secret: String,
47 epoch_authenticator: String,
48 external_secret: String,
49 confirmation_key: String,
50 membership_key: String,
51 resumption_psk: String,
52
53 external_pub: String,
54 exporter: Exporter,
55}
56
57#[derive(Serialize, Deserialize, Debug, Clone, Default)]
58pub struct KeyScheduleTestVector {
59 pub cipher_suite: u16,
60 group_id: String,
61 initial_init_secret: String,
62 epochs: Vec<Epoch>,
63}
64
65#[allow(clippy::type_complexity)]
67fn generate(
68 ciphersuite: Ciphersuite,
69 init_secret: &InitSecret,
70 group_id: &[u8],
71 epoch: u64,
72 crypto: &impl OpenMlsProvider,
73) -> (
74 Vec<u8>,
75 CommitSecret,
76 PskSecret,
77 JoinerSecret,
78 WelcomeSecret,
79 EpochSecrets,
80 Vec<u8>,
81 GroupContext,
82 HpkeKeyPair,
83) {
84 let tree_hash = crypto
85 .rand()
86 .random_vec(ciphersuite.hash_length())
87 .expect("An unexpected error occurred.");
88 let commit_secret = CommitSecret::random(ciphersuite, crypto.rand());
89
90 let confirmed_transcript_hash = crypto
91 .rand()
92 .random_vec(ciphersuite.hash_length())
93 .expect("An unexpected error occurred.");
94
95 let a: [u8; 1] = crypto.rand().random_array().unwrap();
97 let psk_secret = if a[0] > 127 {
98 PskSecret::from(Secret::random(ciphersuite, crypto.rand()).unwrap())
99 } else {
100 PskSecret::from(Secret::zero(ciphersuite))
101 };
102
103 let group_context = GroupContext::new(
104 ciphersuite,
105 GroupId::from_slice(group_id),
106 epoch,
107 tree_hash.to_vec(),
108 confirmed_transcript_hash.clone(),
109 Extensions::empty(),
110 );
111
112 let joiner_secret = JoinerSecret::new(
113 crypto.crypto(),
114 ciphersuite,
115 commit_secret.clone(),
116 init_secret,
117 &group_context.tls_serialize_detached().unwrap(),
118 )
119 .expect("Could not create JoinerSecret.");
120 let mut key_schedule = KeySchedule::init(
121 ciphersuite,
122 crypto.crypto(),
123 &joiner_secret,
124 psk_secret.clone(),
125 )
126 .expect("Could not create KeySchedule.");
127 let welcome_secret = key_schedule
128 .welcome(crypto.crypto(), ciphersuite)
129 .expect("An unexpected error occurred.");
130
131 let serialized_group_context = group_context
132 .tls_serialize_detached()
133 .expect("Could not serialize group context.");
134
135 key_schedule
136 .add_context(crypto.crypto(), &serialized_group_context)
137 .expect("An unexpected error occurred.");
138 let EpochSecretsResult { epoch_secrets, .. } = key_schedule
139 .epoch_secrets(crypto.crypto(), ciphersuite)
140 .expect("An unexpected error occurred.");
141
142 let external_key_pair = epoch_secrets
144 .external_secret()
145 .derive_external_keypair(crypto.crypto(), ciphersuite)
146 .expect("An unexpected crypto error occurred.");
147
148 (
149 confirmed_transcript_hash,
150 commit_secret,
151 psk_secret,
152 joiner_secret,
153 welcome_secret,
154 epoch_secrets,
155 tree_hash,
156 group_context,
157 external_key_pair,
158 )
159}
160
161#[cfg(any(feature = "test-utils", test))]
162pub fn generate_test_vector(
163 n_epochs: u64,
164 ciphersuite: Ciphersuite,
165 provider: &impl OpenMlsProvider,
166) -> KeyScheduleTestVector {
167 use tls_codec::Serialize;
168
169 let mut init_secret =
171 InitSecret::random(ciphersuite, provider.rand()).expect("Not enough randomness.");
172 let initial_init_secret = init_secret.clone();
173 let group_id = provider
174 .rand()
175 .random_vec(16)
176 .expect("An unexpected error occurred.");
177
178 let mut epochs = Vec::new();
179
180 for epoch in 0..n_epochs {
182 println!("Generating epoch: {epoch:?}");
183 let (
184 confirmed_transcript_hash,
185 commit_secret,
186 psk_secret,
187 joiner_secret,
188 welcome_secret,
189 epoch_secrets,
190 tree_hash,
191 group_context,
192 external_key_pair,
193 ) = generate(ciphersuite, &init_secret, &group_id, epoch, provider);
194
195 let exporter_label = "exporter label";
197 let exporter_length = 32u32;
198 let exporter_context = b"exporter context";
199 let exported = epoch_secrets
200 .exporter_secret()
201 .derive_exported_secret(
202 ciphersuite,
203 provider.crypto(),
204 exporter_label,
205 exporter_context,
206 exporter_length as usize,
207 )
208 .unwrap();
209
210 let epoch_info = Epoch {
211 tree_hash: bytes_to_hex(&tree_hash),
212 commit_secret: bytes_to_hex(commit_secret.as_slice()),
213 psk_secret: bytes_to_hex(psk_secret.as_slice()),
214 confirmed_transcript_hash: bytes_to_hex(&confirmed_transcript_hash),
215 group_context: bytes_to_hex(
216 &group_context
217 .tls_serialize_detached()
218 .expect("An unexpected error occurred."),
219 ),
220 joiner_secret: bytes_to_hex(joiner_secret.as_slice()),
221 welcome_secret: bytes_to_hex(welcome_secret.as_slice()),
222 init_secret: bytes_to_hex(epoch_secrets.init_secret().as_slice()),
223 sender_data_secret: bytes_to_hex(epoch_secrets.sender_data_secret().as_slice()),
224 encryption_secret: bytes_to_hex(epoch_secrets.encryption_secret().as_slice()),
225 exporter_secret: bytes_to_hex(epoch_secrets.exporter_secret().as_slice()),
226 epoch_authenticator: bytes_to_hex(epoch_secrets.epoch_authenticator().as_slice()),
227 external_secret: bytes_to_hex(epoch_secrets.external_secret().as_slice()),
228 confirmation_key: bytes_to_hex(epoch_secrets.confirmation_key().as_slice()),
229 membership_key: bytes_to_hex(epoch_secrets.membership_key().as_slice()),
230 resumption_psk: bytes_to_hex(epoch_secrets.resumption_psk().as_slice()),
231 external_pub: bytes_to_hex(&external_key_pair.public),
232 exporter: Exporter {
233 label: exporter_label.into(),
234 context: bytes_to_hex(exporter_context),
235 length: exporter_length,
236 secret: bytes_to_hex(exported.as_slice()),
237 },
238 };
239 epochs.push(epoch_info);
240 init_secret = epoch_secrets.init_secret().clone();
241 }
242
243 KeyScheduleTestVector {
244 cipher_suite: ciphersuite as u16,
245 group_id: bytes_to_hex(&group_id),
246 initial_init_secret: bytes_to_hex(initial_init_secret.as_slice()),
247 epochs,
248 }
249}
250
251#[cfg(feature = "generate-kats")]
252#[test]
253fn write_test_vectors() {
254 const NUM_EPOCHS: u64 = 2;
255 let mut tests = Vec::new();
256 let provider = openmls_libcrux_crypto::Provider::default();
257 for &ciphersuite in provider.crypto().supported_ciphersuites().iter() {
258 tests.push(generate_test_vector(NUM_EPOCHS, ciphersuite, &provider));
259 }
260 write("test_vectors/key-schedule-new.json", &tests);
261
262 let pq_tests: Vec<_> = tests
265 .into_iter()
266 .filter(|test| !(0x0001..=0x0007).contains(&test.cipher_suite))
267 .collect();
268 write("test_vectors/key-schedule-pq-new.json", &pq_tests);
269}
270
271#[openmls_test::openmls_test]
272fn read_test_vectors_key_schedule() {
273 let provider = &Provider::default();
274
275 let _ = pretty_env_logger::try_init();
276
277 let tests: Vec<KeyScheduleTestVector> =
278 read_json!("../../../../test_vectors/key-schedule.json");
279
280 for test_vector in tests {
281 match run_test_vector(test_vector, provider) {
282 Ok(_) => {}
283 Err(e) => panic!("Error while checking key schedule test vector.\n{e:?}"),
284 }
285 }
286}
287
288#[cfg(feature = "draft-ietf-mls-pq-ciphersuites")]
293#[openmls_test::openmls_test]
294fn read_test_vectors_key_schedule_pq() {
295 let provider = &Provider::default();
296
297 let _ = pretty_env_logger::try_init();
298
299 let tests: Vec<KeyScheduleTestVector> =
300 read_json!("../../../../test_vectors/key-schedule-pq.json");
301
302 for test_vector in tests {
303 match run_test_vector(test_vector, provider) {
304 Ok(_) => {}
305 Err(e) => panic!("Error while checking key schedule test vector.\n{e:?}"),
306 }
307 }
308}
309
310#[cfg(any(feature = "test-utils", test))]
311pub fn run_test_vector(
312 test_vector: KeyScheduleTestVector,
313 provider: &impl OpenMlsProvider,
314) -> Result<(), KsTestVectorError> {
315 let ciphersuite = Ciphersuite::try_from(test_vector.cipher_suite).expect("Invalid ciphersuite");
316 log::trace!(" {test_vector:?}");
317
318 if !provider
319 .crypto()
320 .supported_ciphersuites()
321 .contains(&ciphersuite)
322 {
323 info!("Skipping unsupported ciphersuite `{ciphersuite:?}`.");
324 return Ok(());
325 }
326
327 let group_id = hex_to_bytes(&test_vector.group_id);
328 let init_secret = hex_to_bytes(&test_vector.initial_init_secret);
329 log::trace!(
330 " InitSecret from tve: {:?}",
331 test_vector.initial_init_secret
332 );
333 let mut init_secret = InitSecret::from(Secret::from_slice(&init_secret));
334
335 for (epoch_ctr, epoch) in test_vector.epochs.iter().enumerate() {
336 let tree_hash = hex_to_bytes(&epoch.tree_hash);
337 let secret = hex_to_bytes(&epoch.commit_secret);
338 let commit_secret = CommitSecret::from(PathSecret::from(Secret::from_slice(&secret)));
339 log::trace!(" CommitSecret from tve {:?}", epoch.commit_secret);
340
341 let confirmed_transcript_hash = hex_to_bytes(&epoch.confirmed_transcript_hash);
342
343 let group_context = GroupContext::new(
344 ciphersuite,
345 GroupId::from_slice(&group_id),
346 GroupEpoch::from(epoch_ctr as u64),
347 tree_hash.to_vec(),
348 confirmed_transcript_hash.clone(),
349 Extensions::empty(),
350 );
351
352 let joiner_secret = JoinerSecret::new(
353 provider.crypto(),
354 ciphersuite,
355 commit_secret,
356 &init_secret,
357 &group_context.tls_serialize_detached().unwrap(),
358 )
359 .expect("Could not create JoinerSecret.");
360 if hex_to_bytes(&epoch.joiner_secret) != joiner_secret.as_slice() {
361 if cfg!(test) {
362 panic!("Joiner secret mismatch");
363 }
364 return Err(KsTestVectorError::JoinerSecretMismatch);
365 }
366
367 let psk_secret_inner = Secret::from_slice(&hex_to_bytes(&epoch.psk_secret));
368 let psk_secret = PskSecret::from(psk_secret_inner);
369
370 let mut key_schedule =
371 KeySchedule::init(ciphersuite, provider.crypto(), &joiner_secret, psk_secret)
372 .expect("Could not create KeySchedule.");
373 let welcome_secret = key_schedule
374 .welcome(provider.crypto(), ciphersuite)
375 .expect("An unexpected error occurred.");
376
377 if hex_to_bytes(&epoch.welcome_secret) != welcome_secret.as_slice() {
378 if cfg!(test) {
379 panic!("Welcome secret mismatch");
380 }
381 return Err(KsTestVectorError::WelcomeSecretMismatch);
382 }
383
384 let expected_group_context = hex_to_bytes(&epoch.group_context);
385 let group_context_serialized = group_context
386 .tls_serialize_detached()
387 .expect("An unexpected error occurred.");
388 if group_context_serialized != expected_group_context {
389 log::error!(" Group context mismatch");
390 log::debug!(" Computed: {group_context_serialized:x?}");
391 log::debug!(" Expected: {expected_group_context:x?}");
392 if cfg!(test) {
393 panic!("Group context mismatch");
394 }
395 return Err(KsTestVectorError::GroupContextMismatch);
396 }
397
398 key_schedule
399 .add_context(provider.crypto(), &group_context_serialized)
400 .expect("An unexpected error occurred.");
401
402 let EpochSecretsResult { epoch_secrets, .. } = key_schedule
403 .epoch_secrets(provider.crypto(), ciphersuite)
404 .expect("An unexpected error occurred.");
405
406 init_secret = epoch_secrets.init_secret().clone();
407 if hex_to_bytes(&epoch.init_secret) != init_secret.as_slice() {
408 log_crypto!(
409 debug,
410 " Epoch secret mismatch: {:x?} != {:x?}",
411 hex_to_bytes(&epoch.init_secret),
412 init_secret.as_slice()
413 );
414 if cfg!(test) {
415 panic!("Init secret mismatch");
416 }
417 return Err(KsTestVectorError::InitSecretMismatch);
418 }
419 if hex_to_bytes(&epoch.sender_data_secret) != epoch_secrets.sender_data_secret().as_slice()
420 {
421 if cfg!(test) {
422 panic!("Sender data secret mismatch");
423 }
424 return Err(KsTestVectorError::SenderDataSecretMismatch);
425 }
426 if hex_to_bytes(&epoch.encryption_secret) != epoch_secrets.encryption_secret().as_slice() {
427 if cfg!(test) {
428 panic!("Encryption secret mismatch");
429 }
430 return Err(KsTestVectorError::EncryptionSecretMismatch);
431 }
432 if hex_to_bytes(&epoch.exporter_secret) != epoch_secrets.exporter_secret().as_slice() {
433 if cfg!(test) {
434 panic!("Exporter secret mismatch");
435 }
436 return Err(KsTestVectorError::ExporterSecretMismatch);
437 }
438 if hex_to_bytes(&epoch.epoch_authenticator)
439 != epoch_secrets.epoch_authenticator().as_slice()
440 {
441 if cfg!(test) {
442 panic!("Epoch authenticator mismatch");
443 }
444 return Err(KsTestVectorError::EpochAuthenticatorMismatch);
445 }
446 if hex_to_bytes(&epoch.external_secret) != epoch_secrets.external_secret().as_slice() {
447 if cfg!(test) {
448 panic!("External secret mismatch");
449 }
450 return Err(KsTestVectorError::ExternalSecretMismatch);
451 }
452 if hex_to_bytes(&epoch.confirmation_key) != epoch_secrets.confirmation_key().as_slice() {
453 if cfg!(test) {
454 panic!("Confirmation key mismatch");
455 }
456 return Err(KsTestVectorError::ConfirmationKeyMismatch);
457 }
458 if hex_to_bytes(&epoch.membership_key) != epoch_secrets.membership_key().as_slice() {
459 if cfg!(test) {
460 panic!("Membership key mismatch");
461 }
462 return Err(KsTestVectorError::MembershipKeyMismatch);
463 }
464 if hex_to_bytes(&epoch.resumption_psk) != epoch_secrets.resumption_psk().as_slice() {
465 if cfg!(test) {
466 panic!("Resumption psk mismatch");
467 }
468 return Err(KsTestVectorError::ResumptionPskMismatch);
469 }
470
471 let external_key_pair = epoch_secrets
473 .external_secret()
474 .derive_external_keypair(provider.crypto(), ciphersuite)
475 .expect("an unexpected crypto error occurred");
476 if hex_to_bytes(&epoch.external_pub) != external_key_pair.public {
477 log::error!(" External public key mismatch");
478 log::debug!(
479 " Computed: {:x?}",
480 HpkePublicKey::from(external_key_pair.public)
481 .tls_serialize_detached()
482 .expect("An unexpected error occurred.")
483 );
484 log::debug!(" Expected: {:x?}", hex_to_bytes(&epoch.external_pub));
485 if cfg!(test) {
486 panic!("External pub mismatch");
487 }
488 return Err(KsTestVectorError::ExternalPubMismatch);
489 }
490
491 let exported = epoch_secrets
493 .exporter_secret()
494 .derive_exported_secret(
495 ciphersuite,
496 provider.crypto(),
497 &epoch.exporter.label,
498 &hex_to_bytes(&epoch.exporter.context),
499 epoch.exporter.length as usize,
500 )
501 .unwrap();
502 if hex_to_bytes(&epoch.exporter.secret) != exported.as_slice() {
503 if cfg!(test) {
504 panic!("Exporter mismatch");
505 }
506 return Err(KsTestVectorError::ExporterMismatch);
507 }
508 }
509 Ok(())
510}