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 @@
+
\ 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);
+ }
+}