Skip to main content

openmls/schedule/tests_and_kats/kats/
key_schedule.rs

1//! # Known Answer Tests for the key schedule
2//!
3//! See <https://github.com/mlswg/mls-implementations/blob/master/test-vectors.md>
4//! for more description on the test vectors.
5//!
6//! If values are not present, they are encoded as empty strings.
7
8use 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    // Chosen by the generator
34    tree_hash: String,
35    commit_secret: String,
36    psk_secret: String,
37    confirmed_transcript_hash: String,
38
39    // Computed values
40    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// Ignore clippy warning since this just used for testing
66#[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    // PSK secret can sometimes be the all zero vector
96    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    // Calculate external HPKE key pair
143    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    // Set up setting.
170    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    // Generate info for all epochs
181    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        // exporter
196        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    // The official vectors in `key-schedule.json` only cover the RFC 9420
263    // ciphersuites (0x0001..=0x0007). The others go into `key-schedule-pq.json`.
264    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/// Vectors for the [draft-ietf-mls-pq-ciphersuites] ciphersuites, generated with
289/// the libcrux provider (see `write_test_vectors`).
290///
291/// [draft-ietf-mls-pq-ciphersuites]: https://datatracker.ietf.org/doc/draft-ietf-mls-pq-ciphersuites
292#[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        // Calculate external HPKE key pair
472        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        // Check exported secret
492        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}