diff --git a/Cargo.lock b/Cargo.lock index 289347a87..87093de82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -319,6 +319,17 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" +[[package]] +name = "classic-mceliece-rust" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45ce62f72a15a9071f83c5084bdf0af4e8cbf31431e79eb4a5509a2f7fe7fe5d" +dependencies = [ + "rand", + "sha3", + "zeroize", +] + [[package]] name = "colorchoice" version = "1.0.1" @@ -1045,6 +1056,18 @@ dependencies = [ "fs_extra", ] +[[package]] +name = "libcrux-psq" +version = "0.0.2-pre.2" +dependencies = [ + "classic-mceliece-rust", + "criterion", + "libcrux-hkdf", + "libcrux-hmac", + "libcrux-kem", + "rand", +] + [[package]] name = "libcrux-sha3" version = "0.0.2-pre.2" diff --git a/Cargo.toml b/Cargo.toml index cb0053d51..5c73a2917 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ members = [ "libcrux-kem", "libcrux-hmac", "libcrux-hkdf", - "libcrux-ecdh", + "libcrux-ecdh", "libcrux-psq", ] [workspace.package] diff --git a/benchmarks/benches/sha2.rs b/benchmarks/benches/sha2.rs index 297dee805..d9f82577e 100644 --- a/benchmarks/benches/sha2.rs +++ b/benchmarks/benches/sha2.rs @@ -10,7 +10,7 @@ macro_rules! impl_comp { ($fun:ident, $libcrux:expr, $ring:expr, $rust_crypto:ty, $openssl:expr) => { // Comparing libcrux performance for different payload sizes and other implementations. fn $fun(c: &mut Criterion) { - const PAYLOAD_SIZES: [usize; 1] = [1024 * 1024 * 10]; + const PAYLOAD_SIZES: [usize; 5] = [100, 1024, 2048, 4096, 8192]; let mut group = c.benchmark_group(stringify!($fun).replace("_", " ")); diff --git a/libcrux-psq/Cargo.toml b/libcrux-psq/Cargo.toml new file mode 100644 index 000000000..e85de5287 --- /dev/null +++ b/libcrux-psq/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "libcrux-psq" +version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +edition.workspace = true +repository.workspace = true +readme.workspace = true + +[lib] +path = "src/psq.rs" + +[dependencies] +libcrux-kem = { version = "0.0.2-pre.2", path = "../libcrux-kem" } +libcrux-hkdf = { version = "=0.0.2-pre.2", path = "../libcrux-hkdf" } +libcrux-hmac = { version = "=0.0.2-pre.2", path = "../libcrux-hmac" } +classic-mceliece-rust = { version = "2.0.0", features = [ + "mceliece460896f", + "zeroize", +] } +rand = { version = "0.8" } + +[dev-dependencies] +criterion = "0.5" + +[[bench]] +name = "psq" +harness = false diff --git a/libcrux-psq/README.md b/libcrux-psq/README.md new file mode 100644 index 000000000..c98576bbe --- /dev/null +++ b/libcrux-psq/README.md @@ -0,0 +1,32 @@ +# Post-Quantum Pre-Shared-Key Protocol (PSQ) # + +This crate implements a protocol for agreeing on a pre-shared key such +that the protocol messages are secure against +harvest-now-decrypt-later (HNDL) passive quantum attackers. + +The protocol between initator `A` and receiver `B` roughly works as follows: +``` +A: (ik, enc) <- PQ-KEM(pk_B) + K_0 <- KDF(ik, pk_B || enc || sctxt) + K_m <- KDF(K_0, "Confirmation") + K <- KDF(K_0, "PSK") + mac_ttl <- MAC(K_m, psk_ttl) +A -> B: (enc, psk_ttl, mac_ttl) +``` +Where +* `pk_B` is the receiver's KEM public key, +* `sctx` is context information for the given session of the protocol, +* `psk_ttl` specifies for how long the PSK should be considered valid, and +* `K` is the final PSK that is derived from the decapsulated shared + secret based on the internal KEM. + +The crate implements the protocol based on several different internal +KEMs: + * `X25519`, an elliptic-curve Diffie-Hellman KEM (not post-quantum + secure; for performance comparison) + * `ML-KEM 768`, a lattice-based post-quantum KEM, in the process + of being standardized by NIST + * `Classic McEliece`, a code-based post-quantum KEM & Round 4 + candidate in the NIST PQ competition, + * `XWingKemDraft02`, a hybrid post-quantum KEM, combining `X25519` + and `ML-KEM 768` based KEMs diff --git a/libcrux-psq/benches/psq.rs b/libcrux-psq/benches/psq.rs new file mode 100644 index 000000000..a25a8b343 --- /dev/null +++ b/libcrux-psq/benches/psq.rs @@ -0,0 +1,334 @@ +use classic_mceliece_rust::{decapsulate_boxed, encapsulate_boxed}; +use rand::thread_rng; +use std::time::Duration; + +use criterion::{criterion_group, criterion_main, BatchSize, Criterion}; + +pub fn comparisons_kem_key_generation(c: &mut Criterion) { + let mut rng = thread_rng(); + let mut group = c.benchmark_group("Raw KEM Key Generation"); + group.measurement_time(Duration::from_secs(15)); + + group.bench_function("libcrux ML-KEM-768", |b| { + b.iter(|| { + let _ = libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rng); + }) + }); + group.bench_function("libcrux X25519", |b| { + b.iter(|| { + let _ = libcrux_kem::key_gen(libcrux_kem::Algorithm::X25519, &mut rng); + }) + }); + group.bench_function("libcrux XWingKemDraft02", |b| { + b.iter(|| { + let _ = libcrux_kem::key_gen(libcrux_kem::Algorithm::XWingKemDraft02, &mut rng); + }) + }); + group.bench_function("classic_mceliece_rust (mceliece460896f)", |b| { + b.iter(|| { + let _ = classic_mceliece_rust::keypair_boxed(&mut rng); + }) + }); +} + +pub fn comparisons_kem_encaps(c: &mut Criterion) { + let mut rng = thread_rng(); + let mut group = c.benchmark_group("Raw KEM Encapsulation"); + group.measurement_time(Duration::from_secs(15)); + + group.bench_function("libcrux ML-KEM-768", |b| { + b.iter_batched( + || libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rng).unwrap(), + |(_sk, pk)| { + let _ = pk.encapsulate(&mut thread_rng()); + }, + BatchSize::SmallInput, + ) + }); + + group.bench_function("libcrux X25519", |b| { + b.iter_batched( + || libcrux_kem::key_gen(libcrux_kem::Algorithm::X25519, &mut rng).unwrap(), + |(_sk, pk)| { + let _ = pk.encapsulate(&mut thread_rng()); + }, + BatchSize::SmallInput, + ) + }); + + group.bench_function("libcrux XWingKemDraft02", |b| { + b.iter_batched( + || libcrux_kem::key_gen(libcrux_kem::Algorithm::XWingKemDraft02, &mut rng).unwrap(), + |(_sk, pk)| { + let _ = pk.encapsulate(&mut thread_rng()); + }, + BatchSize::SmallInput, + ) + }); + + group.bench_function("classic_mceliece_rust (mceliece460896f)", |b| { + b.iter_batched( + || classic_mceliece_rust::keypair_boxed(&mut rng), + |(pk, _sk)| { + let _ = encapsulate_boxed(&pk, &mut thread_rng()); + }, + BatchSize::SmallInput, + ) + }); +} + +pub fn comparisons_kem_decaps(c: &mut Criterion) { + let mut rng = thread_rng(); + let mut group = c.benchmark_group("Raw KEM Decapsulation"); + group.measurement_time(Duration::from_secs(15)); + + group.bench_function("libcrux ML-KEM-768", |b| { + b.iter_batched( + || { + let (sk, pk) = + libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rng).unwrap(); + let (_ss, enc) = pk.encapsulate(&mut rng).unwrap(); + (sk, enc) + }, + |(sk, enc)| enc.decapsulate(&sk), + BatchSize::SmallInput, + ) + }); + + group.bench_function("libcrux X25519", |b| { + b.iter_batched( + || { + let (sk, pk) = + libcrux_kem::key_gen(libcrux_kem::Algorithm::X25519, &mut rng).unwrap(); + let (_ss, enc) = pk.encapsulate(&mut rng).unwrap(); + (sk, enc) + }, + |(sk, enc)| enc.decapsulate(&sk), + BatchSize::SmallInput, + ) + }); + + group.bench_function("libcrux XWingKemDraft02", |b| { + b.iter_batched( + || { + let (sk, pk) = + libcrux_kem::key_gen(libcrux_kem::Algorithm::XWingKemDraft02, &mut rng) + .unwrap(); + let (_ss, enc) = pk.encapsulate(&mut rng).unwrap(); + (sk, enc) + }, + |(sk, enc)| enc.decapsulate(&sk), + BatchSize::SmallInput, + ) + }); + + group.bench_function("classic_mceliece_rust (mceliece460896f)", |b| { + b.iter_batched( + || { + let (pk, sk) = classic_mceliece_rust::keypair_boxed(&mut rng); + let (enc, _ss) = encapsulate_boxed(&pk, &mut rng); + (sk, enc) + }, + |(sk, enc)| decapsulate_boxed(&enc, &sk), + BatchSize::SmallInput, + ) + }); +} + +pub fn comparisons_psq_key_generation(c: &mut Criterion) { + let mut rng = thread_rng(); + let mut group = c.benchmark_group("PSK-PQ Key Generation"); + group.measurement_time(Duration::from_secs(15)); + + group.bench_function("libcrux ML-KEM-768", |b| { + b.iter(|| { + let _ = libcrux_psq::generate_key_pair(libcrux_psq::Algorithm::MlKem768, &mut rng); + }) + }); + group.bench_function("libcrux X25519", |b| { + b.iter(|| { + let _ = libcrux_psq::generate_key_pair(libcrux_psq::Algorithm::X25519, &mut rng); + }) + }); + group.bench_function("libcrux XWingKemDraft02", |b| { + b.iter(|| { + let _ = + libcrux_psq::generate_key_pair(libcrux_psq::Algorithm::XWingKemDraft02, &mut rng); + }) + }); + group.bench_function("classic_mceliece_rust (mceliece460896f)", |b| { + b.iter(|| { + let _ = + libcrux_psq::generate_key_pair(libcrux_psq::Algorithm::ClassicMcEliece, &mut rng); + }) + }); +} + +pub fn comparisons_psq_send(c: &mut Criterion) { + let mut rng = thread_rng(); + let mut group = c.benchmark_group("PSK-PQ Pre-Shared Key Send"); + group.measurement_time(Duration::from_secs(15)); + + group.bench_function("libcrux ML-KEM-768", |b| { + b.iter_batched( + || libcrux_psq::generate_key_pair(libcrux_psq::Algorithm::MlKem768, &mut rng).unwrap(), + |(_sk, pk)| { + let _ = pk.send_psk( + b"bench context", + Duration::from_secs(3600), + &mut thread_rng(), + ); + }, + BatchSize::SmallInput, + ) + }); + + group.bench_function("libcrux X25519", |b| { + b.iter_batched( + || libcrux_psq::generate_key_pair(libcrux_psq::Algorithm::X25519, &mut rng).unwrap(), + |(_sk, pk)| { + let _ = pk.send_psk( + b"bench context", + Duration::from_secs(3600), + &mut thread_rng(), + ); + }, + BatchSize::SmallInput, + ) + }); + + group.bench_function("libcrux XWingKemDraft02", |b| { + b.iter_batched( + || { + libcrux_psq::generate_key_pair(libcrux_psq::Algorithm::XWingKemDraft02, &mut rng) + .unwrap() + }, + |(_sk, pk)| { + let _ = pk.send_psk( + b"bench context", + Duration::from_secs(3600), + &mut thread_rng(), + ); + }, + BatchSize::SmallInput, + ) + }); + + group.bench_function("classic_mceliece_rust (mceliece460896f)", |b| { + b.iter_batched( + || { + libcrux_psq::generate_key_pair(libcrux_psq::Algorithm::ClassicMcEliece, &mut rng) + .unwrap() + }, + |(_sk, pk)| { + let _ = pk.send_psk( + b"bench context", + Duration::from_secs(3600), + &mut thread_rng(), + ); + }, + BatchSize::SmallInput, + ) + }); +} + +pub fn comparisons_psq_receive(c: &mut Criterion) { + let mut rng = thread_rng(); + let mut group = c.benchmark_group("PSK-PQ Pre-Shared Key Receive"); + group.measurement_time(Duration::from_secs(15)); + + group.bench_function("libcrux ML-KEM-768", |b| { + b.iter_batched( + || { + let (sk, pk) = + libcrux_psq::generate_key_pair(libcrux_psq::Algorithm::MlKem768, &mut rng) + .unwrap(); + + let (_psk, message) = pk + .send_psk(b"bench context", Duration::from_secs(3600), &mut rng) + .unwrap(); + (pk, sk, message) + }, + |(pk, sk, message)| { + let _ = sk.receive_psk(&pk, &message, b"bench context"); + }, + BatchSize::SmallInput, + ) + }); + + group.bench_function("libcrux X25519", |b| { + b.iter_batched( + || { + let (sk, pk) = + libcrux_psq::generate_key_pair(libcrux_psq::Algorithm::X25519, &mut rng) + .unwrap(); + + let (_psk, message) = pk + .send_psk(b"bench context", Duration::from_secs(3600), &mut rng) + .unwrap(); + (pk, sk, message) + }, + |(pk, sk, message)| { + let _ = sk.receive_psk(&pk, &message, b"bench context"); + }, + BatchSize::SmallInput, + ) + }); + + group.bench_function("libcrux XWingKemDraft02", |b| { + b.iter_batched( + || { + let (sk, pk) = libcrux_psq::generate_key_pair( + libcrux_psq::Algorithm::XWingKemDraft02, + &mut rng, + ) + .unwrap(); + + let (_psk, message) = pk + .send_psk(b"bench context", Duration::from_secs(3600), &mut rng) + .unwrap(); + (pk, sk, message) + }, + |(pk, sk, message)| { + let _ = sk.receive_psk(&pk, &message, b"bench context"); + }, + BatchSize::SmallInput, + ) + }); + + group.bench_function("classic_mceliece_rust (mceliece460896f)", |b| { + b.iter_batched( + || { + let (sk, pk) = libcrux_psq::generate_key_pair( + libcrux_psq::Algorithm::ClassicMcEliece, + &mut rng, + ) + .unwrap(); + + let (_psk, message) = pk + .send_psk(b"bench context", Duration::from_secs(3600), &mut rng) + .unwrap(); + (pk, sk, message) + }, + |(pk, sk, message)| { + let _ = sk.receive_psk(&pk, &message, b"bench context"); + }, + BatchSize::SmallInput, + ) + }); +} + +pub fn comparisons(c: &mut Criterion) { + // Raw KEM operations + comparisons_kem_key_generation(c); + comparisons_kem_encaps(c); + comparisons_kem_decaps(c); + + // PSQ protocol + comparisons_psq_key_generation(c); + comparisons_psq_send(c); + comparisons_psq_receive(c); +} + +criterion_group!(benches, comparisons); +criterion_main!(benches); diff --git a/libcrux-psq/examples/encaps.rs b/libcrux-psq/examples/encaps.rs new file mode 100644 index 000000000..6c26ca3d3 --- /dev/null +++ b/libcrux-psq/examples/encaps.rs @@ -0,0 +1,17 @@ +use std::time::Duration; + +use libcrux_psq::{generate_key_pair, Algorithm}; +use rand::thread_rng; + +fn main() { + let mut rng = thread_rng(); + let mlkem_keypair = generate_key_pair(Algorithm::MlKem768, &mut rng).unwrap(); + + for _ in 0..100_000 { + let _ = core::hint::black_box(mlkem_keypair.1.send_psk( + b"size context", + Duration::from_secs(3600), + &mut rng, + )); + } +} diff --git a/libcrux-psq/examples/sizes.rs b/libcrux-psq/examples/sizes.rs new file mode 100644 index 000000000..b2fbf2e7d --- /dev/null +++ b/libcrux-psq/examples/sizes.rs @@ -0,0 +1,67 @@ +use std::time::Duration; + +use libcrux_psq::*; +use rand::{self, thread_rng}; + +fn main() { + let mut rng = thread_rng(); + let mlkem_keypair = generate_key_pair(Algorithm::MlKem768, &mut rng).unwrap(); + let x25519_keypair = generate_key_pair(Algorithm::X25519, &mut rng).unwrap(); + let xwing_keypair = generate_key_pair(Algorithm::XWingKemDraft02, &mut rng).unwrap(); + let classic_mceliece_keypair = generate_key_pair(Algorithm::ClassicMcEliece, &mut rng).unwrap(); + + let mlkem_message = mlkem_keypair + .1 + .send_psk(b"size context", Duration::from_secs(3600), &mut rng) + .unwrap(); + let x25519_message = x25519_keypair + .1 + .send_psk(b"size context", Duration::from_secs(3600), &mut rng) + .unwrap(); + let xwing_message = xwing_keypair + .1 + .send_psk(b"size context", Duration::from_secs(3600), &mut rng) + .unwrap(); + let classic_mceliece_message = classic_mceliece_keypair + .1 + .send_psk(b"size context", Duration::from_secs(3600), &mut rng) + .unwrap(); + + println!("ML-KEM-768:"); + println!(" Public key size (bytes): {}", mlkem_keypair.1.size()); + println!(" Message size (bytes): {}", mlkem_message.1.size()); + println!( + " including ciphertext size (bytes): {}", + mlkem_message.1.ct_size() + ); + + println!("X25519:"); + println!(" Public key size (bytes): {}", x25519_keypair.1.size()); + println!(" Message size (bytes): {}", x25519_message.1.size()); + println!( + " including ciphertext size (bytes): {}", + x25519_message.1.ct_size() + ); + + println!("XWingKemDraft02:"); + println!(" Public key size (bytes): {}", xwing_keypair.1.size()); + println!(" Message size (bytes): {}", xwing_message.1.size()); + println!( + " including ciphertext size (bytes): {}", + xwing_message.1.ct_size() + ); + + println!("Classic McEliece:"); + println!( + " Public key size (bytes): {}", + classic_mceliece_keypair.1.size() + ); + println!( + " Message size (bytes): {}", + classic_mceliece_message.1.size() + ); + println!( + " including ciphertext size (bytes): {}", + classic_mceliece_message.1.ct_size() + ); +} diff --git a/libcrux-psq/flamegraph.svg b/libcrux-psq/flamegraph.svg new file mode 100644 index 000000000..9a50187c4 --- /dev/null +++ b/libcrux-psq/flamegraph.svg @@ -0,0 +1,491 @@ +Flame Graph Reset ZoomSearch encaps`DYLD-STUB$$memcpy (1 samples, 0.04%)encaps`Hacl_Hash_SHA2_sha256_finish (1 samples, 0.04%)encaps`Hacl_Hash_SHA2_sha256_update_last (1 samples, 0.04%)encaps`Hacl_Hash_SHA2_sha256_update_nblocks (6 samples, 0.22%)encaps`sha256_update (1,169 samples, 42.65%)encaps`sha256_updatelibsystem_c.dylib`__memcpy_chk (2 samples, 0.07%)encaps`Hacl_HMAC_compute_sha2_256 (1,184 samples, 43.20%)encaps`Hacl_HMAC_compute_sha2_256libsystem_platform.dylib`_platform_memmove (1 samples, 0.04%)encaps`Hacl_Hash_SHA2_sha256_update_nblocks (1 samples, 0.04%)libsystem_c.dylib`DYLD-STUB$$mkdtempat_np (1 samples, 0.04%)libsystem_platform.dylib`_platform_memmove (1 samples, 0.04%)encaps`Hacl_HKDF_expand_sha2_256 (1,193 samples, 43.52%)encaps`Hacl_HKDF_expand_sha2_256libsystem_pthread.dylib`___chkstk_darwin (4 samples, 0.15%)encaps`Hacl_Hash_SHA2_sha256_finish (1 samples, 0.04%)encaps`sha256_update (91 samples, 3.32%)enc..libsystem_c.dylib`__memcpy_chk (1 samples, 0.04%)encaps`Hacl_HMAC_compute_sha2_256 (94 samples, 3.43%)enc..libsystem_platform.dylib`_platform_memmove (1 samples, 0.04%)libsystem_malloc.dylib`_nanov2_free (1 samples, 0.04%)libsystem_malloc.dylib`nanov2_malloc (1 samples, 0.04%)libsystem_malloc.dylib`nanov2_realloc (1 samples, 0.04%)libsystem_malloc.dylib`_nanov2_free (1 samples, 0.04%)libsystem_malloc.dylib`szone_realloc (2 samples, 0.07%)libsystem_malloc.dylib`szone_malloc_should_clear (1 samples, 0.04%)libsystem_malloc.dylib`small_malloc_should_clear (1 samples, 0.04%)libsystem_malloc.dylib`_malloc_zone_realloc (8 samples, 0.29%)libsystem_platform.dylib`_platform_memmove (3 samples, 0.11%)libsystem_malloc.dylib`default_zone_realloc (1 samples, 0.04%)libsystem_malloc.dylib`nanov2_realloc (1 samples, 0.04%)encaps`alloc::raw_vec::RawVec<T,A>::reserve::do_reserve_and_handle (11 samples, 0.40%)encaps`alloc::raw_vec::finish_grow (11 samples, 0.40%)libsystem_malloc.dylib`_realloc (11 samples, 0.40%)libsystem_malloc.dylib`szone_size (1 samples, 0.04%)libsystem_malloc.dylib`tiny_size (1 samples, 0.04%)libsystem_malloc.dylib`small_malloc_should_clear (2 samples, 0.07%)encaps`libcrux_kem::Ct::encode (4 samples, 0.15%)libsystem_malloc.dylib`szone_malloc_should_clear (2 samples, 0.07%)libsystem_malloc.dylib`small_malloc_should_clear (2 samples, 0.07%)libsystem_malloc.dylib`small_malloc_from_free_list (1 samples, 0.04%)libsystem_malloc.dylib`small_free_list_remove_ptr_no_clear (1 samples, 0.04%)encaps`<rand_chacha::chacha::ChaCha12Core as rand_core::block::BlockRngCore>::generate (12 samples, 0.44%)encaps`DYLD-STUB$$memcpy (1 samples, 0.04%)encaps`libcrux_ml_kem::ind_cpa::encrypt (1,058 samples, 38.60%)encaps`libcrux_ml_kem::ind_cpa::encryptlibsystem_platform.dylib`_platform_memmove (31 samples, 1.13%)encaps`libcrux_ml_kem::mlkem768::encapsulate (1,365 samples, 49.80%)encaps`libcrux_ml_kem::mlkem768::encapsulatelibsystem_platform.dylib`_platform_memset (21 samples, 0.77%)encaps`libcrux_kem::PublicKey::encapsulate (1,380 samples, 50.35%)encaps`libcrux_kem::PublicKey::encapsulatelibsystem_platform.dylib`_platform_memmove (2 samples, 0.07%)encaps`libcrux_kem::Ss::encode (1 samples, 0.04%)encaps`libcrux_ml_kem::mlkem768::encapsulate (2 samples, 0.07%)libsystem_c.dylib`clock_gettime (5 samples, 0.18%)libsystem_c.dylib`gettimeofday (5 samples, 0.18%)libsystem_kernel.dylib`mach_absolute_time (5 samples, 0.18%)libsystem_malloc.dylib`_malloc_zone_malloc (2 samples, 0.07%)libsystem_malloc.dylib`_nanov2_free (2 samples, 0.07%)libsystem_malloc.dylib`_szone_free (1 samples, 0.04%)libsystem_malloc.dylib`free_small (1 samples, 0.04%)libsystem_malloc.dylib`nanov2_calloc (1 samples, 0.04%)libsystem_platform.dylib`__bzero (1 samples, 0.04%)libsystem_platform.dylib`_platform_memmove (7 samples, 0.26%)libsystem_platform.dylib`_platform_memset (4 samples, 0.15%)encaps`encaps::main (2,716 samples, 99.09%)encaps`encaps::mainlibsystem_pthread.dylib`___chkstk_darwin (2 samples, 0.07%)libsystem_malloc.dylib`_free (1 samples, 0.04%)dyld`start (2,740 samples, 99.96%)dyld`startencaps`main (2,740 samples, 99.96%)encaps`mainencaps`std::sys_common::backtrace::__rust_begin_short_backtrace (2,740 samples, 99.96%)encaps`std::sys_common::backtrace::__rust_begin_short_backtracelibsystem_platform.dylib`_platform_memmove (23 samples, 0.84%)all (2,741 samples, 100%)libsystem_kernel.dylib`__exit (1 samples, 0.04%) \ No newline at end of file diff --git a/libcrux-psq/src/psq.rs b/libcrux-psq/src/psq.rs new file mode 100644 index 000000000..cedb78f80 --- /dev/null +++ b/libcrux-psq/src/psq.rs @@ -0,0 +1,492 @@ +//! # PQ-PSK establishment protocol +//! +//! This crate implements a post-quantum (PQ) pre-shared key (PSK) establishment +//! protocol. + +#![deny(missing_docs)] +use std::time::{Duration, SystemTime}; + +use classic_mceliece_rust::{decapsulate_boxed, encapsulate_boxed}; +use libcrux_hmac::hmac; +use rand::{CryptoRng, Rng}; + +const PSK_LENGTH: usize = 32; +const K0_LENGTH: usize = 32; +const KM_LENGTH: usize = 32; +const MAC_LENGTH: usize = 32; + +const CONFIRMATION_CONTEXT: &[u8] = b"Confirmation"; +const PSK_CONTEXT: &[u8] = b"PSK"; + +type Psk = [u8; PSK_LENGTH]; +type Mac = [u8; MAC_LENGTH]; + +#[derive(Debug)] +/// PSQ Errors. +pub enum Error { + /// An invalid public key was provided + InvalidPublicKey, + /// An invalid private key was provided + InvalidPrivateKey, + /// An error during PSK encapsulation + GenerationError, + /// An error during PSK decapsulation + DerivationError, +} + +/// The algorithm that should be used for the internal KEM. +pub enum Algorithm { + /// An elliptic-curve Diffie-Hellman based KEM (Does not provide post-quantum security) + X25519, + /// ML-KEM 768, a lattice-based post-quantum KEM, as specified in FIPS 203 (Draft) + MlKem768, + /// A code-based post-quantum KEM & Round 4 candidate in the NIST PQ competition (Parameter Set `mceliece460896f`) + ClassicMcEliece, + /// A hybrid post-quantum KEM combining X25519 and ML-KEM 768 + XWingKemDraft02, +} + +enum Ciphertext { + X25519(libcrux_kem::Ct), + MlKem768(libcrux_kem::Ct), + XWingKemDraft02(libcrux_kem::Ct), + ClassicMcEliece(classic_mceliece_rust::Ciphertext), +} + +/// A PSQ public key +pub enum PublicKey<'a> { + /// for use with X25519-based protocol + X25519(libcrux_kem::PublicKey), + /// for use with ML-KEM-768-based protocol + MlKem768(libcrux_kem::PublicKey), + /// for use with hybrid KEM XWingDraft02-based protocol + XWingKemDraft02(libcrux_kem::PublicKey), + /// for use with Classic McEliece-based protocol + ClassicMcEliece(classic_mceliece_rust::PublicKey<'a>), +} + +/// A PSQ private key +pub enum PrivateKey<'a> { + /// for use with X25519-based protocol + X25519(libcrux_kem::PrivateKey), + /// for use with ML-KEM-768-based protocol + MlKem768(libcrux_kem::PrivateKey), + /// for use with hybrid KEM XWingDraft02-based protocol + XWingKemDraft02(libcrux_kem::PrivateKey), + /// for use with Classic McEliece-based protocol + ClassicMcEliece(classic_mceliece_rust::SecretKey<'a>), +} + +enum SharedSecret<'a> { + X25519(libcrux_kem::Ss), + MlKem768(libcrux_kem::Ss), + XWingKemDraft02(libcrux_kem::Ss), + ClassicMcEliece(classic_mceliece_rust::SharedSecret<'a>), +} + +impl SharedSecret<'_> { + fn encode(&self) -> Vec { + match self { + SharedSecret::X25519(ss) => ss.encode(), + SharedSecret::MlKem768(ss) => ss.encode(), + SharedSecret::ClassicMcEliece(ss) => ss.as_ref().to_owned(), + SharedSecret::XWingKemDraft02(ss) => ss.encode(), + } + } +} + +impl Ciphertext { + fn encode(&self) -> Vec { + match self { + Ciphertext::X25519(ct) => ct.encode(), + Ciphertext::MlKem768(ct) => ct.encode(), + Ciphertext::ClassicMcEliece(ct) => ct.as_ref().to_owned(), + Ciphertext::XWingKemDraft02(ct) => ct.encode(), + } + } + fn decapsulate(&self, sk: &PrivateKey) -> Result { + match self { + Ciphertext::X25519(ct) => { + if let PrivateKey::X25519(sk) = sk { + let ss = ct.decapsulate(sk).unwrap(); + Ok(SharedSecret::X25519(ss)) + } else { + Err(Error::InvalidPrivateKey) + } + } + Ciphertext::MlKem768(ct) => { + if let PrivateKey::MlKem768(sk) = sk { + let ss = ct.decapsulate(sk).unwrap(); + Ok(SharedSecret::MlKem768(ss)) + } else { + Err(Error::InvalidPrivateKey) + } + } + Ciphertext::ClassicMcEliece(ct) => { + if let PrivateKey::ClassicMcEliece(sk) = sk { + let ss = decapsulate_boxed(ct, sk); + Ok(SharedSecret::ClassicMcEliece(ss)) + } else { + Err(Error::InvalidPrivateKey) + } + } + Ciphertext::XWingKemDraft02(ct) => { + if let PrivateKey::XWingKemDraft02(sk) = sk { + let ss = ct.decapsulate(sk).unwrap(); + Ok(SharedSecret::XWingKemDraft02(ss)) + } else { + Err(Error::InvalidPrivateKey) + } + } + } + } +} + +impl From for libcrux_kem::Algorithm { + fn from(val: Algorithm) -> Self { + match val { + Algorithm::X25519 => libcrux_kem::Algorithm::X25519, + Algorithm::MlKem768 => libcrux_kem::Algorithm::MlKem768, + Algorithm::ClassicMcEliece => { + unimplemented!("libcrux does not support Classic McEliece") + } + Algorithm::XWingKemDraft02 => libcrux_kem::Algorithm::XWingKemDraft02, + } + } +} + +/// Generate a PSQ key pair. +pub fn generate_key_pair( + alg: Algorithm, + rng: &mut (impl CryptoRng + Rng), +) -> Result<(PrivateKey<'static>, PublicKey<'static>), Error> { + match alg { + Algorithm::X25519 => { + let (sk, pk) = libcrux_kem::key_gen(alg.into(), rng).unwrap(); + Ok((PrivateKey::X25519(sk), PublicKey::X25519(pk))) + } + Algorithm::MlKem768 => { + let (sk, pk) = libcrux_kem::key_gen(alg.into(), rng).unwrap(); + Ok((PrivateKey::MlKem768(sk), PublicKey::MlKem768(pk))) + } + Algorithm::ClassicMcEliece => { + let (pk, sk) = classic_mceliece_rust::keypair_boxed(rng); + Ok(( + PrivateKey::ClassicMcEliece(sk), + PublicKey::ClassicMcEliece(pk), + )) + } + Algorithm::XWingKemDraft02 => { + let (sk, pk) = libcrux_kem::key_gen(alg.into(), rng).unwrap(); + Ok(( + PrivateKey::XWingKemDraft02(sk), + PublicKey::XWingKemDraft02(pk), + )) + } + } +} + +impl PublicKey<'_> { + /// Return the size (in bytes) of the PSQ public key. + pub fn size(&self) -> usize { + self.encode().len() + } + pub(crate) fn encode(&self) -> Vec { + match self { + PublicKey::X25519(k) => k.encode(), + PublicKey::MlKem768(k) => k.encode(), + PublicKey::XWingKemDraft02(k) => k.encode(), + PublicKey::ClassicMcEliece(k) => k.as_ref().to_vec(), + } + } + + /// Use the underlying KEM to encapsulate a shared secret towards the receiver. + pub(crate) fn encapsulate( + &self, + rng: &mut (impl CryptoRng + Rng), + ) -> Result<(SharedSecret, Ciphertext), Error> { + match self { + PublicKey::X25519(pk) => { + let (ss, enc) = pk.encapsulate(rng).unwrap(); + Ok((SharedSecret::X25519(ss), Ciphertext::X25519(enc))) + } + PublicKey::MlKem768(pk) => { + let (ss, enc) = pk.encapsulate(rng).unwrap(); + Ok((SharedSecret::MlKem768(ss), Ciphertext::MlKem768(enc))) + } + PublicKey::ClassicMcEliece(pk) => { + let (enc, ss) = encapsulate_boxed(pk, rng); + Ok(( + SharedSecret::ClassicMcEliece(ss), + Ciphertext::ClassicMcEliece(enc), + )) + } + PublicKey::XWingKemDraft02(pk) => { + let (ss, enc) = pk.encapsulate(rng).unwrap(); + Ok(( + SharedSecret::XWingKemDraft02(ss), + Ciphertext::XWingKemDraft02(enc), + )) + } + } + } + + /// Generate a fresh PSK, and a message encapsulating it for the + /// receiver. + /// + /// The encapsulated PSK is valid for the given duration + /// `psk_ttl`, based on milliseconds since the UNIX epoch until + /// current system time. Parameter `sctx` is used to + /// cryptographically bind the generated PSK to a given outer + /// protocol context and may be considered public. + pub fn send_psk( + &self, + sctx: &[u8], + psk_ttl: Duration, + rng: &mut (impl CryptoRng + Rng), + ) -> Result<(Psk, PskMessage), Error> { + let (ik, enc) = self.encapsulate(rng).map_err(|_| Error::GenerationError)?; + let mut info = self.encode(); + info.extend_from_slice(&enc.encode()); + info.extend_from_slice(sctx); + + let k0 = libcrux_hkdf::expand( + libcrux_hkdf::Algorithm::Sha256, + ik.encode(), + info, + K0_LENGTH, + ) + .map_err(|_| Error::GenerationError)?; + + let km = libcrux_hkdf::expand( + libcrux_hkdf::Algorithm::Sha256, + &k0, + CONFIRMATION_CONTEXT, + KM_LENGTH, + ) + .map_err(|_| Error::GenerationError)?; + + let psk: Psk = libcrux_hkdf::expand( + libcrux_hkdf::Algorithm::Sha256, + &k0, + PSK_CONTEXT, + PSK_LENGTH, + ) + .map_err(|_| Error::GenerationError)? + .try_into() + .expect("should receive the correct number of bytes from HKDF"); + + let now = SystemTime::now(); + let ts = now.duration_since(SystemTime::UNIX_EPOCH).unwrap(); + let ts_seconds = ts.as_secs(); + let ts_subsec_millis = ts.subsec_millis(); + let mut mac_input = ts_seconds.to_be_bytes().to_vec(); + mac_input.extend_from_slice(&ts_subsec_millis.to_be_bytes()); + mac_input.extend_from_slice(&psk_ttl.as_millis().to_be_bytes()); + + let mac: Mac = hmac( + libcrux_hmac::Algorithm::Sha256, + &km, + &mac_input, + Some(MAC_LENGTH), + ) + .try_into() + .expect("should receive the correct number of bytes from HMAC"); + + Ok(( + psk, + PskMessage { + enc, + ts: (ts_seconds, ts_subsec_millis), + psk_ttl, + mac, + }, + )) + } +} + +impl PrivateKey<'_> { + /// Derive a PSK from a PSQ message. + /// + /// Can error, if the given PSQ message is invalid, i.e. beyond + /// its TTL or cryptographically invalid. + pub fn receive_psk( + &self, + pk: &PublicKey, + message: &PskMessage, + sctx: &[u8], + ) -> Result { + let PskMessage { + enc, + ts: (ts_seconds, ts_subsec_millis), + psk_ttl, + mac, + } = message; + + let now = SystemTime::now(); + let ts_since_epoch = + Duration::from_secs(*ts_seconds) + Duration::from_millis((*ts_subsec_millis).into()); + if now.duration_since(SystemTime::UNIX_EPOCH).unwrap() - ts_since_epoch >= *psk_ttl { + Err(Error::DerivationError) + } else { + let ik = enc.decapsulate(self).map_err(|_| Error::DerivationError)?; + + let mut info = pk.encode(); + info.extend_from_slice(&enc.encode()); + info.extend_from_slice(sctx); + + let k0 = libcrux_hkdf::expand( + libcrux_hkdf::Algorithm::Sha256, + ik.encode(), + info, + K0_LENGTH, + ) + .map_err(|_| Error::DerivationError)?; + + let km = libcrux_hkdf::expand( + libcrux_hkdf::Algorithm::Sha256, + &k0, + CONFIRMATION_CONTEXT, + KM_LENGTH, + ) + .map_err(|_| Error::DerivationError)?; + + let mut mac_input = ts_seconds.to_be_bytes().to_vec(); + mac_input.extend_from_slice(&ts_subsec_millis.to_be_bytes()); + mac_input.extend_from_slice(&psk_ttl.as_millis().to_be_bytes()); + + let recomputed_mac: Mac = hmac( + libcrux_hmac::Algorithm::Sha256, + &km, + &mac_input, + Some(MAC_LENGTH), + ) + .try_into() + .expect("should receive the correct number of bytes from HMAC"); + + if recomputed_mac != *mac { + Err(Error::DerivationError) + } else { + let psk: Psk = libcrux_hkdf::expand( + libcrux_hkdf::Algorithm::Sha256, + &k0, + PSK_CONTEXT, + PSK_LENGTH, + ) + .map_err(|_| Error::DerivationError)? + .try_into() + .expect("should receive the correct number of bytes from HKDF"); + + Ok(psk) + } + } + } +} + +/// A message that encapsulates as post-quantum PSK of a certain +/// lifetime, tied to a specific outer protocol context. +pub struct PskMessage { + enc: Ciphertext, + ts: (u64, u32), + psk_ttl: Duration, + mac: Mac, +} + +impl PskMessage { + /// Returns the size (in bytes) of the ciphertext enclosed in the message. + pub fn ct_size(&self) -> usize { + self.enc.encode().len() + } + /// Returns the total size (in bytes) of the message. + pub fn size(&self) -> usize { + self.ct_size() + + MAC_LENGTH // self.mac.len() + + 8 // self.ts.to_be_bytes().len() + + 8 // self.psk_ttl.num_milliseconds().to_be_bytes().len() + } +} + +#[cfg(test)] +mod tests { + use std::thread::sleep; + + use super::*; + + #[test] + fn simple_x25519() { + let mut rng = rand::thread_rng(); + let (sk, pk) = generate_key_pair(Algorithm::X25519, &mut rng).unwrap(); + let sctx = b"test context"; + let (psk_initiator, message) = pk + .send_psk(sctx, Duration::from_secs(2 * 3600), &mut rng) + .unwrap(); + + let psk_responder = sk.receive_psk(&pk, &message, sctx).unwrap(); + assert_eq!(psk_initiator, psk_responder); + } + + #[test] + #[should_panic] + fn zero_ttl() { + let mut rng = rand::thread_rng(); + let (sk, pk) = generate_key_pair(Algorithm::X25519, &mut rng).unwrap(); + let sctx = b"test context"; + let (_psk_initiator, message) = + pk.send_psk(sctx, Duration::from_secs(0), &mut rng).unwrap(); + + let _psk_responder = sk.receive_psk(&pk, &message, sctx).unwrap(); + } + + #[test] + #[should_panic] + fn expired_timestamp() { + let mut rng = rand::thread_rng(); + let (sk, pk) = generate_key_pair(Algorithm::X25519, &mut rng).unwrap(); + let sctx = b"test context"; + let (_psk_initiator, message) = + pk.send_psk(sctx, Duration::from_secs(1), &mut rng).unwrap(); + + sleep(Duration::from_secs(2)); + + let _psk_responder = sk.receive_psk(&pk, &message, sctx).unwrap(); + } + + #[test] + fn simple_mlkem768() { + let mut rng = rand::thread_rng(); + let (sk, pk) = generate_key_pair(Algorithm::MlKem768, &mut rng).unwrap(); + let sctx = b"test context"; + let (psk_initiator, message) = pk + .send_psk(sctx, Duration::from_secs(2 * 3600), &mut rng) + .unwrap(); + + let psk_responder = sk.receive_psk(&pk, &message, sctx).unwrap(); + assert_eq!(psk_initiator, psk_responder); + } + + #[test] + fn simple_xwing() { + let mut rng = rand::thread_rng(); + let (sk, pk) = generate_key_pair(Algorithm::XWingKemDraft02, &mut rng).unwrap(); + let sctx = b"test context"; + let (psk_initiator, message) = pk + .send_psk(sctx, Duration::from_secs(2 * 3600), &mut rng) + .unwrap(); + + let psk_responder = sk.receive_psk(&pk, &message, sctx).unwrap(); + assert_eq!(psk_initiator, psk_responder); + } + + #[test] + fn simple_classic_mceliece() { + let mut rng = rand::thread_rng(); + let (sk, pk) = generate_key_pair(Algorithm::ClassicMcEliece, &mut rng).unwrap(); + let sctx = b"test context"; + let (psk_initiator, message) = pk + .send_psk(sctx, Duration::from_secs(2 * 3600), &mut rng) + .unwrap(); + + let psk_responder = sk.receive_psk(&pk, &message, sctx).unwrap(); + assert_eq!(psk_initiator, psk_responder); + } +}