From 3088e6be6442212365eae66942a4fe061bd27136 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Thu, 8 Oct 2026 09:24:02 +0200 Subject: [PATCH 01/17] build: update to the latest version of our openmls fork This version supports PQ ciphersuites. --- Cargo.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4a3aeecd9d8..bd21bd07655 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -131,10 +131,10 @@ reqwest = { version = "0.13", default-features = false, features = [ testcontainers = { version = "0.28", features = ["reusable-containers"] } # our OpenMLS fork -openmls = { git = "https://github.com/wireapp/openmls", rev = "9252caa031d874b2e6e2157db6b8024882eb72f3", version = "1" } -openmls_basic_credential = { git = "https://github.com/wireapp/openmls", rev = "9252caa031d874b2e6e2157db6b8024882eb72f3", version = "0.2" } -openmls_traits = { git = "https://github.com/wireapp/openmls", rev = "9252caa031d874b2e6e2157db6b8024882eb72f3", version = "0.2" } -openmls_x509_credential = { git = "https://github.com/wireapp/openmls", rev = "9252caa031d874b2e6e2157db6b8024882eb72f3", version = "0.2" } +openmls = { git = "https://github.com/wireapp/openmls", rev = "70311ef0fdb6bf69d344d08eb9b73feb473ba19b", version = "1" } +openmls_basic_credential = { git = "https://github.com/wireapp/openmls", rev = "70311ef0fdb6bf69d344d08eb9b73feb473ba19b", version = "0.2" } +openmls_traits = { git = "https://github.com/wireapp/openmls", rev = "70311ef0fdb6bf69d344d08eb9b73feb473ba19b", version = "0.2" } +openmls_x509_credential = { git = "https://github.com/wireapp/openmls", rev = "70311ef0fdb6bf69d344d08eb9b73feb473ba19b", version = "0.2" } # proteus proteus-traits = { git = "https://github.com/wireapp/proteus", tag = "v3.0.1" } From 2cf1155d4d1be43533d0e0a8a53ba64a084128d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Thu, 8 Oct 2026 09:58:50 +0200 Subject: [PATCH 02/17] build: crypto: depend on ml-dsa --- crypto/Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crypto/Cargo.toml b/crypto/Cargo.toml index cf2328e2ebc..396a59018ed 100644 --- a/crypto/Cargo.toml +++ b/crypto/Cargo.toml @@ -78,6 +78,10 @@ rand.workspace = true getrandom.workspace = true rand_core = "0.10" rand_chacha = "0.10" +ml-dsa = { version = "0.1", default-features = false, features = [ + "alloc", + "zeroize", +] } [target.'cfg(target_os = "unknown")'.dependencies] serde-wasm-bindgen.workspace = true From 9840c9eb074c6e21501fad3598539063780d882b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Thu, 8 Oct 2026 10:32:39 +0200 Subject: [PATCH 03/17] build: crypto: depend on crypto-common We're going to need its KeySizeUser trait. --- crypto/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/crypto/Cargo.toml b/crypto/Cargo.toml index 396a59018ed..300b89de9d3 100644 --- a/crypto/Cargo.toml +++ b/crypto/Cargo.toml @@ -82,6 +82,7 @@ ml-dsa = { version = "0.1", default-features = false, features = [ "alloc", "zeroize", ] } +crypto-common = "0.2" [target.'cfg(target_os = "unknown")'.dependencies] serde-wasm-bindgen.workspace = true From 4f960d3415ae9f9eacf5e428877e4e60acccbf0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Thu, 8 Oct 2026 11:30:18 +0200 Subject: [PATCH 04/17] build: crypto: depend on blake3 We're going to use it for ML-DSA thumbprints. --- crypto/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/crypto/Cargo.toml b/crypto/Cargo.toml index 300b89de9d3..0c4ed9d16dd 100644 --- a/crypto/Cargo.toml +++ b/crypto/Cargo.toml @@ -83,6 +83,7 @@ ml-dsa = { version = "0.1", default-features = false, features = [ "zeroize", ] } crypto-common = "0.2" +blake3 = "1.8" [target.'cfg(target_os = "unknown")'.dependencies] serde-wasm-bindgen.workspace = true From 97d51610533d7e087117ae63363e6ab9ebed55cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Thu, 8 Oct 2026 09:56:13 +0200 Subject: [PATCH 05/17] build: update Cargo.lock --- Cargo.lock | 56 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7e5607d2441..e784bbd59b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -163,6 +163,12 @@ dependencies = [ "password-hash", ] +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + [[package]] name = "askama" version = "0.13.1" @@ -562,6 +568,19 @@ dependencies = [ "digest 0.11.2", ] +[[package]] +name = "blake3" +version = "1.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae" +dependencies = [ + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.0", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -1065,6 +1084,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + [[package]] name = "convert_case" version = "0.10.0" @@ -1123,10 +1148,12 @@ dependencies = [ "async-recursion", "async-trait", "base64 0.23.1", + "blake3", "chacha20poly1305", "const_format", "core-crypto-keystore", "core-crypto-macros", + "crypto-common 0.2.2", "der 0.8.2", "derive_more", "ecdsa 0.17.0", @@ -1142,6 +1169,7 @@ dependencies = [ "itertools 0.15.0", "log", "macro_rules_attribute", + "ml-dsa", "obfuscate", "openmls", "openmls_basic_credential", @@ -3501,6 +3529,22 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "ml-dsa" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "add6b9d92e496f16f4526d68ff29da1483aba4b119baeab8bed3b9e3544a6f3d" +dependencies = [ + "crypto-common 0.2.2", + "ctutils", + "hybrid-array", + "module-lattice", + "pkcs8 0.11.0", + "shake", + "signature 3.0.0", + "zeroize", +] + [[package]] name = "ml-kem" version = "0.3.2" @@ -3768,7 +3812,7 @@ dependencies = [ [[package]] name = "openmls" version = "1.0.0" -source = "git+https://github.com/wireapp/openmls?rev=9252caa031d874b2e6e2157db6b8024882eb72f3#9252caa031d874b2e6e2157db6b8024882eb72f3" +source = "git+https://github.com/wireapp/openmls?rev=70311ef0fdb6bf69d344d08eb9b73feb473ba19b#70311ef0fdb6bf69d344d08eb9b73feb473ba19b" dependencies = [ "async-trait", "backtrace", @@ -3790,12 +3834,13 @@ dependencies = [ [[package]] name = "openmls_basic_credential" version = "0.2.0" -source = "git+https://github.com/wireapp/openmls?rev=9252caa031d874b2e6e2157db6b8024882eb72f3#9252caa031d874b2e6e2157db6b8024882eb72f3" +source = "git+https://github.com/wireapp/openmls?rev=70311ef0fdb6bf69d344d08eb9b73feb473ba19b#70311ef0fdb6bf69d344d08eb9b73feb473ba19b" dependencies = [ "async-trait", "ed25519-dalek 3.0.0", "elliptic-curve 0.14.1", "getrandom 0.4.3", + "ml-dsa", "openmls_traits", "p256 0.14.0", "p384 0.14.0", @@ -3809,10 +3854,11 @@ dependencies = [ [[package]] name = "openmls_traits" version = "0.2.0" -source = "git+https://github.com/wireapp/openmls?rev=9252caa031d874b2e6e2157db6b8024882eb72f3#9252caa031d874b2e6e2157db6b8024882eb72f3" +source = "git+https://github.com/wireapp/openmls?rev=70311ef0fdb6bf69d344d08eb9b73feb473ba19b#70311ef0fdb6bf69d344d08eb9b73feb473ba19b" dependencies = [ "async-trait", "ed25519-dalek 3.0.0", + "ml-dsa", "p256 0.14.0", "p384 0.14.0", "p521 0.14.0", @@ -3826,7 +3872,7 @@ dependencies = [ [[package]] name = "openmls_x509_credential" version = "0.2.0" -source = "git+https://github.com/wireapp/openmls?rev=9252caa031d874b2e6e2157db6b8024882eb72f3#9252caa031d874b2e6e2157db6b8024882eb72f3" +source = "git+https://github.com/wireapp/openmls?rev=70311ef0fdb6bf69d344d08eb9b73feb473ba19b#70311ef0fdb6bf69d344d08eb9b73feb473ba19b" dependencies = [ "async-trait", "base64 0.21.7", @@ -5871,7 +5917,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.3", "once_cell", "rustix 1.1.4", "windows-sys 0.61.2", From 96436a17a66a6323ce91fcd4f3d9fba8902f00fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Thu, 8 Oct 2026 10:08:14 +0200 Subject: [PATCH 06/17] chore: crypto: use macros to dispatch to the correct HPKE functions --- crypto/src/mls_provider/crypto_provider.rs | 329 +++++---------------- 1 file changed, 71 insertions(+), 258 deletions(-) diff --git a/crypto/src/mls_provider/crypto_provider.rs b/crypto/src/mls_provider/crypto_provider.rs index 75f6d4ced00..20ac6befc3e 100644 --- a/crypto/src/mls_provider/crypto_provider.rs +++ b/crypto/src/mls_provider/crypto_provider.rs @@ -46,6 +46,36 @@ impl Default for RustCrypto { } } +macro_rules! hpke_dispatch { + ($config:expr, $f:ident $(, $arg:expr)* $(,)?) => { + match $config { + HpkeConfig(HpkeKemType::DhKem25519, HpkeKdfType::HkdfSha256, HpkeAeadType::AesGcm128) => + hpke_core::$f::($($arg),*), + HpkeConfig(HpkeKemType::DhKem25519, HpkeKdfType::HkdfSha256, HpkeAeadType::ChaCha20Poly1305) => + hpke_core::$f::($($arg),*), + HpkeConfig(HpkeKemType::DhKemP256, HpkeKdfType::HkdfSha256, HpkeAeadType::AesGcm128) => + hpke_core::$f::($($arg),*), + HpkeConfig(HpkeKemType::DhKemP384, HpkeKdfType::HkdfSha384, HpkeAeadType::AesGcm256) => + hpke_core::$f::($($arg),*), + HpkeConfig(HpkeKemType::DhKemP521, HpkeKdfType::HkdfSha512, HpkeAeadType::AesGcm256) => + hpke_core::$f::($($arg),*), + _ => Err(CryptoError::UnsupportedKem), + } + }; +} + +macro_rules! hpke_kem_dispatch { + ($kem:expr, $f:ident $(, $arg:expr)* $(,)?) => { + match $kem { + HpkeKemType::DhKem25519 => hpke_core::$f::($($arg),*), + HpkeKemType::DhKemP256 => hpke_core::$f::($($arg),*), + HpkeKemType::DhKemP384 => hpke_core::$f::($($arg),*), + HpkeKemType::DhKemP521 => hpke_core::$f::($($arg),*), + HpkeKemType::DhKem448 => Err(CryptoError::UnsupportedKem), + } + }; +} + impl RustCrypto { pub(crate) fn new_with_seed(seed: EntropySeed) -> Self { Self { @@ -81,37 +111,7 @@ impl RustCrypto { ) -> Result { validate_psk(psk, psk_id)?; let mut rng = self.rng.write().map_err(|_| CryptoError::InsufficientRandomness)?; - - match config { - HpkeConfig(HpkeKemType::DhKem25519, HpkeKdfType::HkdfSha256, HpkeAeadType::AesGcm128) => { - hpke_core::hpke_seal_psk::( - pk_r, info, aad, psk, psk_id, ptxt, &mut *rng, - ) - } - HpkeConfig(HpkeKemType::DhKem25519, HpkeKdfType::HkdfSha256, HpkeAeadType::ChaCha20Poly1305) => { - hpke_core::hpke_seal_psk::< - hpke::aead::ChaCha20Poly1305, - hpke::kdf::HkdfSha256, - hpke::kem::X25519HkdfSha256, - >(pk_r, info, aad, psk, psk_id, ptxt, &mut *rng) - } - HpkeConfig(HpkeKemType::DhKemP256, HpkeKdfType::HkdfSha256, HpkeAeadType::AesGcm128) => { - hpke_core::hpke_seal_psk::( - pk_r, info, aad, psk, psk_id, ptxt, &mut *rng, - ) - } - HpkeConfig(HpkeKemType::DhKemP384, HpkeKdfType::HkdfSha384, HpkeAeadType::AesGcm256) => { - hpke_core::hpke_seal_psk::( - pk_r, info, aad, psk, psk_id, ptxt, &mut *rng, - ) - } - HpkeConfig(HpkeKemType::DhKemP521, HpkeKdfType::HkdfSha512, HpkeAeadType::AesGcm256) => { - hpke_core::hpke_seal_psk::( - pk_r, info, aad, psk, psk_id, ptxt, &mut *rng, - ) - } - _ => Err(CryptoError::UnsupportedKem), - } + hpke_dispatch!(config, hpke_seal_psk, pk_r, info, aad, psk, psk_id, ptxt, &mut *rng) } #[expect(clippy::too_many_arguments)] @@ -126,68 +126,17 @@ impl RustCrypto { psk_id: &[u8], ) -> Result, CryptoError> { validate_psk(psk, psk_id)?; - match config { - HpkeConfig(HpkeKemType::DhKem25519, HpkeKdfType::HkdfSha256, HpkeAeadType::AesGcm128) => { - hpke_core::hpke_open_psk::( - sk_r, - input.kem_output.as_slice(), - info, - aad, - psk, - psk_id, - input.ciphertext.as_slice(), - ) - } - HpkeConfig(HpkeKemType::DhKem25519, HpkeKdfType::HkdfSha256, HpkeAeadType::ChaCha20Poly1305) => { - hpke_core::hpke_open_psk::< - hpke::aead::ChaCha20Poly1305, - hpke::kdf::HkdfSha256, - hpke::kem::X25519HkdfSha256, - >( - sk_r, - input.kem_output.as_slice(), - info, - aad, - psk, - psk_id, - input.ciphertext.as_slice(), - ) - } - HpkeConfig(HpkeKemType::DhKemP256, HpkeKdfType::HkdfSha256, HpkeAeadType::AesGcm128) => { - hpke_core::hpke_open_psk::( - sk_r, - input.kem_output.as_slice(), - info, - aad, - psk, - psk_id, - input.ciphertext.as_slice(), - ) - } - HpkeConfig(HpkeKemType::DhKemP384, HpkeKdfType::HkdfSha384, HpkeAeadType::AesGcm256) => { - hpke_core::hpke_open_psk::( - sk_r, - input.kem_output.as_slice(), - info, - aad, - psk, - psk_id, - input.ciphertext.as_slice(), - ) - } - HpkeConfig(HpkeKemType::DhKemP521, HpkeKdfType::HkdfSha512, HpkeAeadType::AesGcm256) => { - hpke_core::hpke_open_psk::( - sk_r, - input.kem_output.as_slice(), - info, - aad, - psk, - psk_id, - input.ciphertext.as_slice(), - ) - } - _ => Err(CryptoError::UnsupportedKem), - } + hpke_dispatch!( + config, + hpke_open_psk, + sk_r, + input.kem_output.as_slice(), + info, + aad, + psk, + psk_id, + input.ciphertext.as_slice(), + ) } } @@ -481,35 +430,7 @@ impl OpenMlsCrypto for RustCrypto { ptxt: &[u8], ) -> Result { let mut rng = self.rng.write().map_err(|_| CryptoError::InsufficientRandomness)?; - - match config { - HpkeConfig(HpkeKemType::DhKem25519, HpkeKdfType::HkdfSha256, HpkeAeadType::AesGcm128) => { - hpke_core::hpke_seal::( - pk_r, info, aad, ptxt, &mut *rng, - ) - } - HpkeConfig(HpkeKemType::DhKem25519, HpkeKdfType::HkdfSha256, HpkeAeadType::ChaCha20Poly1305) => { - hpke_core::hpke_seal::( - pk_r, info, aad, ptxt, &mut *rng, - ) - } - HpkeConfig(HpkeKemType::DhKemP256, HpkeKdfType::HkdfSha256, HpkeAeadType::AesGcm128) => { - hpke_core::hpke_seal::( - pk_r, info, aad, ptxt, &mut *rng, - ) - } - HpkeConfig(HpkeKemType::DhKemP384, HpkeKdfType::HkdfSha384, HpkeAeadType::AesGcm256) => { - hpke_core::hpke_seal::( - pk_r, info, aad, ptxt, &mut *rng, - ) - } - HpkeConfig(HpkeKemType::DhKemP521, HpkeKdfType::HkdfSha512, HpkeAeadType::AesGcm256) => { - hpke_core::hpke_seal::( - pk_r, info, aad, ptxt, &mut *rng, - ) - } - _ => Err(CryptoError::UnsupportedKem), - } + hpke_dispatch!(config, hpke_seal, pk_r, info, aad, ptxt, &mut *rng) } fn hpke_open( @@ -520,56 +441,15 @@ impl OpenMlsCrypto for RustCrypto { info: &[u8], aad: &[u8], ) -> Result, CryptoError> { - let plaintext = match config { - HpkeConfig(HpkeKemType::DhKem25519, HpkeKdfType::HkdfSha256, HpkeAeadType::AesGcm128) => { - hpke_core::hpke_open::( - sk_r, - input.kem_output.as_slice(), - info, - aad, - input.ciphertext.as_slice(), - )? - } - HpkeConfig(HpkeKemType::DhKem25519, HpkeKdfType::HkdfSha256, HpkeAeadType::ChaCha20Poly1305) => { - hpke_core::hpke_open::( - sk_r, - input.kem_output.as_slice(), - info, - aad, - input.ciphertext.as_slice(), - )? - } - HpkeConfig(HpkeKemType::DhKemP256, HpkeKdfType::HkdfSha256, HpkeAeadType::AesGcm128) => { - hpke_core::hpke_open::( - sk_r, - input.kem_output.as_slice(), - info, - aad, - input.ciphertext.as_slice(), - )? - } - HpkeConfig(HpkeKemType::DhKemP384, HpkeKdfType::HkdfSha384, HpkeAeadType::AesGcm256) => { - hpke_core::hpke_open::( - sk_r, - input.kem_output.as_slice(), - info, - aad, - input.ciphertext.as_slice(), - )? - } - HpkeConfig(HpkeKemType::DhKemP521, HpkeKdfType::HkdfSha512, HpkeAeadType::AesGcm256) => { - hpke_core::hpke_open::( - sk_r, - input.kem_output.as_slice(), - info, - aad, - input.ciphertext.as_slice(), - )? - } - _ => return Err(CryptoError::UnsupportedKem), - }; - - Ok(plaintext) + hpke_dispatch!( + config, + hpke_open, + sk_r, + input.kem_output.as_slice(), + info, + aad, + input.ciphertext.as_slice(), + ) } fn hpke_setup_sender_and_export( @@ -581,46 +461,15 @@ impl OpenMlsCrypto for RustCrypto { exporter_length: usize, ) -> Result<(Vec, ExporterSecret), CryptoError> { let mut rng = self.rng.write().map_err(|_| CryptoError::InsufficientRandomness)?; - - let (kem_output, export) = - match config { - HpkeConfig(HpkeKemType::DhKem25519, HpkeKdfType::HkdfSha256, HpkeAeadType::AesGcm128) => { - hpke_core::hpke_export_tx::< - hpke::aead::AesGcm128, - hpke::kdf::HkdfSha256, - hpke::kem::X25519HkdfSha256, - >(pk_r, info, exporter_context, exporter_length, &mut *rng)? - } - HpkeConfig(HpkeKemType::DhKem25519, HpkeKdfType::HkdfSha256, HpkeAeadType::ChaCha20Poly1305) => { - hpke_core::hpke_export_tx::< - hpke::aead::ChaCha20Poly1305, - hpke::kdf::HkdfSha256, - hpke::kem::X25519HkdfSha256, - >(pk_r, info, exporter_context, exporter_length, &mut *rng)? - } - HpkeConfig(HpkeKemType::DhKemP256, HpkeKdfType::HkdfSha256, HpkeAeadType::AesGcm128) => { - hpke_core::hpke_export_tx::< - hpke::aead::AesGcm128, - hpke::kdf::HkdfSha256, - hpke::kem::DhP256HkdfSha256, - >(pk_r, info, exporter_context, exporter_length, &mut *rng)? - } - HpkeConfig(HpkeKemType::DhKemP384, HpkeKdfType::HkdfSha384, HpkeAeadType::AesGcm256) => { - hpke_core::hpke_export_tx::< - hpke::aead::AesGcm256, - hpke::kdf::HkdfSha384, - hpke::kem::DhP384HkdfSha384, - >(pk_r, info, exporter_context, exporter_length, &mut *rng)? - } - HpkeConfig(HpkeKemType::DhKemP521, HpkeKdfType::HkdfSha512, HpkeAeadType::AesGcm256) => { - hpke_core::hpke_export_tx::< - hpke::aead::AesGcm256, - hpke::kdf::HkdfSha512, - hpke::kem::DhP521HkdfSha512, - >(pk_r, info, exporter_context, exporter_length, &mut *rng)? - } - _ => return Err(CryptoError::UnsupportedKem), - }; + let (kem_output, export) = hpke_dispatch!( + config, + hpke_export_tx, + pk_r, + info, + exporter_context, + exporter_length, + &mut *rng, + )?; debug_assert_eq!(export.len(), exporter_length); @@ -636,45 +485,15 @@ impl OpenMlsCrypto for RustCrypto { exporter_context: &[u8], exporter_length: usize, ) -> Result { - let export = - match config { - HpkeConfig(HpkeKemType::DhKem25519, HpkeKdfType::HkdfSha256, HpkeAeadType::AesGcm128) => { - hpke_core::hpke_export_rx::< - hpke::aead::AesGcm128, - hpke::kdf::HkdfSha256, - hpke::kem::X25519HkdfSha256, - >(enc, sk_r, info, exporter_context, exporter_length)? - } - HpkeConfig(HpkeKemType::DhKem25519, HpkeKdfType::HkdfSha256, HpkeAeadType::ChaCha20Poly1305) => { - hpke_core::hpke_export_rx::< - hpke::aead::ChaCha20Poly1305, - hpke::kdf::HkdfSha256, - hpke::kem::X25519HkdfSha256, - >(enc, sk_r, info, exporter_context, exporter_length)? - } - HpkeConfig(HpkeKemType::DhKemP256, HpkeKdfType::HkdfSha256, HpkeAeadType::AesGcm128) => { - hpke_core::hpke_export_rx::< - hpke::aead::AesGcm128, - hpke::kdf::HkdfSha256, - hpke::kem::DhP256HkdfSha256, - >(enc, sk_r, info, exporter_context, exporter_length)? - } - HpkeConfig(HpkeKemType::DhKemP384, HpkeKdfType::HkdfSha384, HpkeAeadType::AesGcm256) => { - hpke_core::hpke_export_rx::< - hpke::aead::AesGcm256, - hpke::kdf::HkdfSha384, - hpke::kem::DhP384HkdfSha384, - >(enc, sk_r, info, exporter_context, exporter_length)? - } - HpkeConfig(HpkeKemType::DhKemP521, HpkeKdfType::HkdfSha512, HpkeAeadType::AesGcm256) => { - hpke_core::hpke_export_rx::< - hpke::aead::AesGcm256, - hpke::kdf::HkdfSha512, - hpke::kem::DhP521HkdfSha512, - >(enc, sk_r, info, exporter_context, exporter_length)? - } - _ => return Err(CryptoError::UnsupportedKem), - }; + let export = hpke_dispatch!( + config, + hpke_export_rx, + enc, + sk_r, + info, + exporter_context, + exporter_length, + )?; debug_assert_eq!(export.len(), exporter_length); @@ -682,13 +501,7 @@ impl OpenMlsCrypto for RustCrypto { } fn derive_hpke_keypair(&self, config: HpkeConfig, ikm: &[u8]) -> Result { - match config.0 { - HpkeKemType::DhKemP256 => hpke_core::hpke_derive_keypair::(ikm), - HpkeKemType::DhKemP384 => hpke_core::hpke_derive_keypair::(ikm), - HpkeKemType::DhKemP521 => hpke_core::hpke_derive_keypair::(ikm), - HpkeKemType::DhKem25519 => hpke_core::hpke_derive_keypair::(ikm), - _ => Err(CryptoError::UnsupportedKem), - } + hpke_kem_dispatch!(config.0, hpke_derive_keypair, ikm) } } From 5ec0ddc1925aa2f445b88f3e03fcbb2fdfd18c50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Thu, 8 Oct 2026 10:25:50 +0200 Subject: [PATCH 07/17] chore: crypto: add PQ HPKE variants to the dispatch macros --- crypto/src/mls_provider/crypto_provider.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/crypto/src/mls_provider/crypto_provider.rs b/crypto/src/mls_provider/crypto_provider.rs index 20ac6befc3e..b36de64e9e1 100644 --- a/crypto/src/mls_provider/crypto_provider.rs +++ b/crypto/src/mls_provider/crypto_provider.rs @@ -59,6 +59,22 @@ macro_rules! hpke_dispatch { hpke_core::$f::($($arg),*), HpkeConfig(HpkeKemType::DhKemP521, HpkeKdfType::HkdfSha512, HpkeAeadType::AesGcm256) => hpke_core::$f::($($arg),*), + HpkeConfig(HpkeKemType::MlKem768X25519, HpkeKdfType::HkdfSha256, HpkeAeadType::AesGcm128) => + hpke_core::$f::($($arg),*), + HpkeConfig(HpkeKemType::MlKem768X25519, HpkeKdfType::HkdfSha384, HpkeAeadType::AesGcm256) => + hpke_core::$f::($($arg),*), + HpkeConfig(HpkeKemType::MlKem768X25519, HpkeKdfType::HkdfSha384, HpkeAeadType::ChaCha20Poly1305) => + hpke_core::$f::($($arg),*), + HpkeConfig(HpkeKemType::MlKem768P256, HpkeKdfType::HkdfSha256, HpkeAeadType::AesGcm128) => + hpke_core::$f::($($arg),*), + HpkeConfig(HpkeKemType::MlKem768P256, HpkeKdfType::HkdfSha384, HpkeAeadType::AesGcm256) => + hpke_core::$f::($($arg),*), + HpkeConfig(HpkeKemType::MlKem1024P384, HpkeKdfType::HkdfSha384, HpkeAeadType::AesGcm256) => + hpke_core::$f::($($arg),*), + HpkeConfig(HpkeKemType::MlKem768, HpkeKdfType::HkdfSha384, HpkeAeadType::AesGcm256) => + hpke_core::$f::($($arg),*), + HpkeConfig(HpkeKemType::MlKem1024, HpkeKdfType::HkdfSha384, HpkeAeadType::AesGcm256) => + hpke_core::$f::($($arg),*), _ => Err(CryptoError::UnsupportedKem), } }; @@ -71,6 +87,11 @@ macro_rules! hpke_kem_dispatch { HpkeKemType::DhKemP256 => hpke_core::$f::($($arg),*), HpkeKemType::DhKemP384 => hpke_core::$f::($($arg),*), HpkeKemType::DhKemP521 => hpke_core::$f::($($arg),*), + HpkeKemType::MlKem768X25519 => hpke_core::$f::($($arg),*), + HpkeKemType::MlKem768P256 => hpke_core::$f::($($arg),*), + HpkeKemType::MlKem1024P384 => hpke_core::$f::($($arg),*), + HpkeKemType::MlKem768 => hpke_core::$f::($($arg),*), + HpkeKemType::MlKem1024 => hpke_core::$f::($($arg),*), HpkeKemType::DhKem448 => Err(CryptoError::UnsupportedKem), } }; From cb4b98f611d6137eaab424a35a0567a2e7997d37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Thu, 8 Oct 2026 10:35:58 +0200 Subject: [PATCH 08/17] feat: crypto: support ML-DSA signature schemes --- crypto/src/mls_provider/crypto_provider.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crypto/src/mls_provider/crypto_provider.rs b/crypto/src/mls_provider/crypto_provider.rs index b36de64e9e1..ae1e3f33559 100644 --- a/crypto/src/mls_provider/crypto_provider.rs +++ b/crypto/src/mls_provider/crypto_provider.rs @@ -5,11 +5,14 @@ use aes_gcm::{ aead::{Aead, Nonce, Payload}, }; use chacha20poly1305::ChaCha20Poly1305; +use crypto_common::KeySizeUser as _; use elliptic_curve::{Generate as _, sec1}; use hkdf::Hkdf; +use ml_dsa::{MlDsa44, MlDsa65, MlDsa87}; use openmls::prelude::HpkeCiphertext; use openmls_traits::{ crypto::OpenMlsCrypto, + mldsa, random::OpenMlsRand, types::{ self, AeadType, Ciphersuite, CryptoError, ExporterSecret, HashType, HpkeAeadType, HpkeConfig, HpkeKdfType, @@ -190,6 +193,9 @@ impl OpenMlsCrypto for RustCrypto { .message_len(::FieldBytesSize::to_usize()), SignatureScheme::ED25519 => ed25519_dalek::PUBLIC_KEY_LENGTH, SignatureScheme::ED448 => 57, + SignatureScheme::MLDSA44 => ml_dsa::VerifyingKey::::key_size(), + SignatureScheme::MLDSA65 => ml_dsa::VerifyingKey::::key_size(), + SignatureScheme::MLDSA87 => ml_dsa::VerifyingKey::::key_size(), } } @@ -369,6 +375,9 @@ impl OpenMlsCrypto for RustCrypto { let pk = k.verifying_key(); Ok((k.to_bytes().into(), pk.to_bytes().into())) } + SignatureScheme::MLDSA44 => mldsa::key_gen::(&mut *rng), + SignatureScheme::MLDSA65 => mldsa::key_gen::(&mut *rng), + SignatureScheme::MLDSA87 => mldsa::key_gen::(&mut *rng), _ => Err(CryptoError::UnsupportedSignatureScheme), } } @@ -390,6 +399,9 @@ impl OpenMlsCrypto for RustCrypto { SignatureScheme::ED448 => { return Err(CryptoError::UnsupportedSignatureScheme); } + SignatureScheme::MLDSA44 | SignatureScheme::MLDSA65 | SignatureScheme::MLDSA87 => { + (self.signature_public_key_len(alg) == key.len()).ok_or(CryptoError::InvalidKey)? + } } Ok(()) } @@ -434,6 +446,9 @@ impl OpenMlsCrypto for RustCrypto { k.verify_strict(data, &sig).map_err(|_| CryptoError::InvalidSignature) } + SignatureScheme::MLDSA44 => mldsa::verify::(data, pk, signature), + SignatureScheme::MLDSA65 => mldsa::verify::(data, pk, signature), + SignatureScheme::MLDSA87 => mldsa::verify::(data, pk, signature), _ => Err(CryptoError::UnsupportedSignatureScheme), } } From dec57d8ccff32845f54458b7ce875315cc586346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Thu, 8 Oct 2026 10:59:03 +0200 Subject: [PATCH 09/17] chore: crypto: handle ML-DSA signature schemes in a few places --- crypto/src/mls/credential/ext.rs | 18 ++++++++++++++++++ crypto/src/mls/credential/x509.rs | 5 +++++ 2 files changed, 23 insertions(+) diff --git a/crypto/src/mls/credential/ext.rs b/crypto/src/mls/credential/ext.rs index 4bbb36975fa..259cc425185 100644 --- a/crypto/src/mls/credential/ext.rs +++ b/crypto/src/mls/credential/ext.rs @@ -134,6 +134,18 @@ impl CredentialExt for openmls::prelude::Certificate { } } +fn compute_thumbprint_mldsa(scheme: SignatureScheme, key: &[u8]) -> String { + let mut hasher = blake3::Hasher::new(); + match scheme { + SignatureScheme::MLDSA44 => hasher.update(b"MLDSA44"), + SignatureScheme::MLDSA65 => hasher.update(b"MLDSA65"), + SignatureScheme::MLDSA87 => hasher.update(b"MLDSA87"), + _ => unreachable!(), + }; + hasher.update(key); + hasher.finalize().to_string() +} + fn compute_thumbprint(cs: CipherSuite, raw_key: &[u8]) -> Result { let sign_alg = match cs.signature_algorithm() { SignatureScheme::ED25519 => JwsAlgorithm::Ed25519, @@ -141,6 +153,12 @@ fn compute_thumbprint(cs: CipherSuite, raw_key: &[u8]) -> Result { SignatureScheme::ECDSA_SECP384R1_SHA384 => JwsAlgorithm::P384, SignatureScheme::ECDSA_SECP521R1_SHA512 => JwsAlgorithm::P521, SignatureScheme::ED448 => return Err(Error::UnsupportedAlgorithm), + scheme @ (SignatureScheme::MLDSA44 | SignatureScheme::MLDSA65 | SignatureScheme::MLDSA87) => { + // There is no JOSE key type for ML-DSA yet. + // We're going to change the way we compute thumbprints anyway, but for the time being + // just provide something sensible. + return Ok(compute_thumbprint_mldsa(scheme, raw_key)); + } }; let hash_alg = match cs.hash_algorithm() { HashType::Sha2_256 => HashAlgorithm::SHA256, diff --git a/crypto/src/mls/credential/x509.rs b/crypto/src/mls/credential/x509.rs index 27368b500dd..ee572b10867 100644 --- a/crypto/src/mls/credential/x509.rs +++ b/crypto/src/mls/credential/x509.rs @@ -84,6 +84,11 @@ impl CertificateBundle { SignatureScheme::ECDSA_SECP256R1_SHA256 | SignatureScheme::ED25519 => HashAlgorithm::SHA256, SignatureScheme::ECDSA_SECP384R1_SHA384 => HashAlgorithm::SHA384, SignatureScheme::ED448 | SignatureScheme::ECDSA_SECP521R1_SHA512 => HashAlgorithm::SHA512, + SignatureScheme::MLDSA44 | SignatureScheme::MLDSA65 | SignatureScheme::MLDSA87 => { + // TODO: use SHA-256 for the time being; thumbprints are soon going to be + // ciphersuite-independent at which point this whole block will go away. + HashAlgorithm::SHA256 + } }; let identity = leaf From b3ce692c968b21a37449f639e41b28e70d85b634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Thu, 8 Oct 2026 10:08:14 +0200 Subject: [PATCH 10/17] feat: crypto: advertise the 11 PQ ciphersuites in leaf node capabilities --- crypto/src/mls/conversation/config.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crypto/src/mls/conversation/config.rs b/crypto/src/mls/conversation/config.rs index ece03629ff7..57815a23028 100644 --- a/crypto/src/mls/conversation/config.rs +++ b/crypto/src/mls/conversation/config.rs @@ -52,6 +52,18 @@ impl ConversationConfiguration { MlsCiphersuite::MLS_128_DHKEMX25519_CHACHA20POLY1305_SHA256_Ed25519, MlsCiphersuite::MLS_256_DHKEMP384_AES256GCM_SHA384_P384, MlsCiphersuite::MLS_256_DHKEMP521_AES256GCM_SHA512_P521, + // PQ ciphersuites from draft-ietf-mls-pq-ciphersuites-06 + MlsCiphersuite::MLS_128_MLKEM768X25519_AES128GCM_SHA256_Ed25519, + MlsCiphersuite::MLS_128_MLKEM768X25519_AES256GCM_SHA384_Ed25519, + MlsCiphersuite::MLS_128_MLKEM768P256_AES128GCM_SHA256_P256, + MlsCiphersuite::MLS_128_MLKEM768P256_AES256GCM_SHA384_P256, + MlsCiphersuite::MLS_192_MLKEM1024P384_AES256GCM_SHA384_P384, + MlsCiphersuite::MLS_128_MLKEM768_AES256GCM_SHA384_P256, + MlsCiphersuite::MLS_192_MLKEM1024_AES256GCM_SHA384_P384, + MlsCiphersuite::MLS_192_MLKEM768_AES256GCM_SHA384_MLDSA65, + MlsCiphersuite::MLS_256_MLKEM1024_AES256GCM_SHA384_MLDSA87, + MlsCiphersuite::MLS_128_MLKEM768_AES256GCM_SHA384_Ed25519, + MlsCiphersuite::MLS_128_MLKEM768X25519_CHACHA20POLY1305_SHA384_MLDSA44, ]; /// Not used at the moment From 0c448a8c9dacf61ed94200570186b064500077e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Thu, 8 Oct 2026 11:31:54 +0200 Subject: [PATCH 11/17] feat: crypto: list all 11 PQ ciphersuites as supported These are all coming from draft-ietf-mls-pq-ciphersuites-06. --- crypto/src/mls_provider/crypto_provider.rs | 26 +++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/crypto/src/mls_provider/crypto_provider.rs b/crypto/src/mls_provider/crypto_provider.rs index ae1e3f33559..bf9b730db77 100644 --- a/crypto/src/mls_provider/crypto_provider.rs +++ b/crypto/src/mls_provider/crypto_provider.rs @@ -205,7 +205,19 @@ impl OpenMlsCrypto for RustCrypto { | Ciphersuite::MLS_128_DHKEMX25519_CHACHA20POLY1305_SHA256_Ed25519 | Ciphersuite::MLS_128_DHKEMP256_AES128GCM_SHA256_P256 | Ciphersuite::MLS_256_DHKEMP384_AES256GCM_SHA384_P384 - | Ciphersuite::MLS_256_DHKEMP521_AES256GCM_SHA512_P521 => Ok(()), + | Ciphersuite::MLS_256_DHKEMP521_AES256GCM_SHA512_P521 + // PQ ciphersuites from draft-ietf-mls-pq-ciphersuites-06 + | Ciphersuite::MLS_128_MLKEM768X25519_AES128GCM_SHA256_Ed25519 + | Ciphersuite::MLS_128_MLKEM768X25519_AES256GCM_SHA384_Ed25519 + | Ciphersuite::MLS_128_MLKEM768P256_AES128GCM_SHA256_P256 + | Ciphersuite::MLS_128_MLKEM768P256_AES256GCM_SHA384_P256 + | Ciphersuite::MLS_192_MLKEM1024P384_AES256GCM_SHA384_P384 + | Ciphersuite::MLS_128_MLKEM768_AES256GCM_SHA384_P256 + | Ciphersuite::MLS_192_MLKEM1024_AES256GCM_SHA384_P384 + | Ciphersuite::MLS_192_MLKEM768_AES256GCM_SHA384_MLDSA65 + | Ciphersuite::MLS_256_MLKEM1024_AES256GCM_SHA384_MLDSA87 + | Ciphersuite::MLS_128_MLKEM768_AES256GCM_SHA384_Ed25519 + | Ciphersuite::MLS_128_MLKEM768X25519_CHACHA20POLY1305_SHA384_MLDSA44 => Ok(()), _ => Err(CryptoError::UnsupportedCiphersuite), } } @@ -217,6 +229,18 @@ impl OpenMlsCrypto for RustCrypto { Ciphersuite::MLS_128_DHKEMP256_AES128GCM_SHA256_P256, Ciphersuite::MLS_256_DHKEMP384_AES256GCM_SHA384_P384, Ciphersuite::MLS_256_DHKEMP521_AES256GCM_SHA512_P521, + // PQ ciphersuites from draft-ietf-mls-pq-ciphersuites-06 + Ciphersuite::MLS_128_MLKEM768X25519_AES128GCM_SHA256_Ed25519, + Ciphersuite::MLS_128_MLKEM768X25519_AES256GCM_SHA384_Ed25519, + Ciphersuite::MLS_128_MLKEM768P256_AES128GCM_SHA256_P256, + Ciphersuite::MLS_128_MLKEM768P256_AES256GCM_SHA384_P256, + Ciphersuite::MLS_192_MLKEM1024P384_AES256GCM_SHA384_P384, + Ciphersuite::MLS_128_MLKEM768_AES256GCM_SHA384_P256, + Ciphersuite::MLS_192_MLKEM1024_AES256GCM_SHA384_P384, + Ciphersuite::MLS_192_MLKEM768_AES256GCM_SHA384_MLDSA65, + Ciphersuite::MLS_256_MLKEM1024_AES256GCM_SHA384_MLDSA87, + Ciphersuite::MLS_128_MLKEM768_AES256GCM_SHA384_Ed25519, + Ciphersuite::MLS_128_MLKEM768X25519_CHACHA20POLY1305_SHA384_MLDSA44, ] } From 4065c7e24c077108f7038fc0af28700cc9ae08e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Thu, 8 Oct 2026 10:20:20 +0200 Subject: [PATCH 12/17] test: crypto: handle newly added PQ signature schemes --- crypto/src/mls/conversation/config.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crypto/src/mls/conversation/config.rs b/crypto/src/mls/conversation/config.rs index 57815a23028..bc48a3a83be 100644 --- a/crypto/src/mls/conversation/config.rs +++ b/crypto/src/mls/conversation/config.rs @@ -239,7 +239,11 @@ mod tests { SignatureScheme::ECDSA_SECP256R1_SHA256 => JwsAlgorithm::P256, SignatureScheme::ECDSA_SECP384R1_SHA384 => JwsAlgorithm::P384, SignatureScheme::ECDSA_SECP521R1_SHA512 => JwsAlgorithm::P521, - SignatureScheme::ED448 => unreachable!(), + // no JWK algorithm for these yet, so nothing to test here + SignatureScheme::ED448 + | SignatureScheme::MLDSA44 + | SignatureScheme::MLDSA65 + | SignatureScheme::MLDSA87 => return, }; let jwk = rusty_jwt_tools::prelude::generate_jwk(alg); From 2f4c7fff7d6a62dfd224031d51392c3018da7d1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Thu, 8 Oct 2026 09:05:18 +0200 Subject: [PATCH 13/17] chore: keystore: ignore PQ ciphersuites in old migrations --- keystore/src/migrations.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/keystore/src/migrations.rs b/keystore/src/migrations.rs index 57b8f1bb046..b3d75261b28 100644 --- a/keystore/src/migrations.rs +++ b/keystore/src/migrations.rs @@ -265,6 +265,19 @@ impl CiphersuiteOccurences { Ciphersuite::MLS_256_DHKEMP521_AES256GCM_SHA512_P521 => None, Ciphersuite::MLS_256_DHKEMX448_CHACHA20POLY1305_SHA512_Ed448 => self.ed448_chacha.into(), Ciphersuite::MLS_256_DHKEMP384_AES256GCM_SHA384_P384 => None, + // the PQ suites postdate these migrations, so they have no legacy + // keystore representation to disambiguate + Ciphersuite::MLS_128_MLKEM768X25519_AES128GCM_SHA256_Ed25519 + | Ciphersuite::MLS_128_MLKEM768X25519_AES256GCM_SHA384_Ed25519 + | Ciphersuite::MLS_128_MLKEM768P256_AES128GCM_SHA256_P256 + | Ciphersuite::MLS_128_MLKEM768P256_AES256GCM_SHA384_P256 + | Ciphersuite::MLS_192_MLKEM1024P384_AES256GCM_SHA384_P384 + | Ciphersuite::MLS_128_MLKEM768_AES256GCM_SHA384_P256 + | Ciphersuite::MLS_192_MLKEM1024_AES256GCM_SHA384_P384 + | Ciphersuite::MLS_192_MLKEM768_AES256GCM_SHA384_MLDSA65 + | Ciphersuite::MLS_256_MLKEM1024_AES256GCM_SHA384_MLDSA87 + | Ciphersuite::MLS_128_MLKEM768_AES256GCM_SHA384_Ed25519 + | Ciphersuite::MLS_128_MLKEM768X25519_CHACHA20POLY1305_SHA384_MLDSA44 => None, } } } @@ -339,6 +352,7 @@ pub(crate) fn make_ciphersuite_for_signature_scheme( } SignatureScheme::ED25519 => ed25519, SignatureScheme::ED448 => ed448, + SignatureScheme::MLDSA44 | SignatureScheme::MLDSA65 | SignatureScheme::MLDSA87 => None, } }; Ok(ciphersuite_for_signature_scheme) From cd67d6192b313416cefc4ed13ed6bef01da6a9db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Thu, 8 Oct 2026 09:05:30 +0200 Subject: [PATCH 14/17] feat: crypto-ffi: add the new PQ ciphersuites --- crypto-ffi/src/cipher_suite.rs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/crypto-ffi/src/cipher_suite.rs b/crypto-ffi/src/cipher_suite.rs index ffdca2696b2..03a91b5c34e 100644 --- a/crypto-ffi/src/cipher_suite.rs +++ b/crypto-ffi/src/cipher_suite.rs @@ -37,6 +37,40 @@ pub enum CipherSuite { /// DH KEM P384 | AES-GCM 256 | SHA2-384 | EcDSA P384 MLS_256_DHKEMP384_AES256GCM_SHA384_P384 = 0x0007, + + // Post-quantum variants using provisional private-use codepoints; not interoperable. + /// ML-KEM-768+X25519 hybrid KEM | AES-GCM 128 | SHA2-256 | Ed25519 + MLS_128_MLKEM768X25519_AES128GCM_SHA256_Ed25519 = 0xF001, + + /// ML-KEM-768+X25519 hybrid KEM | AES-GCM 256 | SHA2-384 | Ed25519 + MLS_128_MLKEM768X25519_AES256GCM_SHA384_Ed25519 = 0xF002, + + /// ML-KEM-768+P-256 hybrid KEM | AES-GCM 128 | SHA2-256 | ECDSA P-256 + MLS_128_MLKEM768P256_AES128GCM_SHA256_P256 = 0xF003, + + /// ML-KEM-768+P-256 hybrid KEM | AES-GCM 256 | SHA2-384 | ECDSA P-256 + MLS_128_MLKEM768P256_AES256GCM_SHA384_P256 = 0xF004, + + /// ML-KEM-1024+P-384 hybrid KEM | AES-GCM 256 | SHA2-384 | ECDSA P-384 + MLS_192_MLKEM1024P384_AES256GCM_SHA384_P384 = 0xF005, + + /// ML-KEM-768 (pure) | AES-GCM 256 | SHA2-384 | Ed25519 + MLS_128_MLKEM768_AES256GCM_SHA384_Ed25519 = 0xF006, + + /// ML-KEM-768 (pure) | AES-GCM 256 | SHA2-384 | ECDSA P-256 + MLS_128_MLKEM768_AES256GCM_SHA384_P256 = 0xF007, + + /// ML-KEM-1024 (pure) | AES-GCM 256 | SHA2-384 | ECDSA P-384 + MLS_192_MLKEM1024_AES256GCM_SHA384_P384 = 0xF008, + + /// ML-KEM-768+X25519 hybrid KEM | ChaCha20Poly1305 | SHA2-384 | ML-DSA-44 + MLS_128_MLKEM768X25519_CHACHA20POLY1305_SHA384_MLDSA44 = 0xF009, + + /// ML-KEM-768 (pure) | AES-GCM 256 | SHA2-384 | ML-DSA-65 + MLS_192_MLKEM768_AES256GCM_SHA384_MLDSA65 = 0xF00A, + + /// ML-KEM-1024 (pure) | AES-GCM 256 | SHA2-384 | ML-DSA-87 + MLS_256_MLKEM1024_AES256GCM_SHA384_MLDSA87 = 0xF00B, } impl From for MlsCipherSuite { From 9d97b3219bb3e8938bf6d75668076f9e0e6469d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Thu, 8 Oct 2026 12:14:21 +0200 Subject: [PATCH 15/17] test: crypto: add all 11 PQ ciphersuites to the test fixture Put all cases behind test-all-cipher, except for - basic and x509 credentials using MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 - basic credentials using MLS_128_MLKEM768X25519_CHACHA20POLY1305_SHA384_MLDSA44 This ensures that with every PR we test at least - one post-quantum KEM with a classical signature scheme - one post-quantum KEM with a post-quantum signature scheme Note that support for post-quantum signatures is not yet available for x509 credentials, so we enable that ciphersuite only for cases with basic credentials. --- crypto/src/test_utils/test_context.rs | 108 ++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/crypto/src/test_utils/test_context.rs b/crypto/src/test_utils/test_context.rs index 790c80ab1cb..7f49eb959bb 100644 --- a/crypto/src/test_utils/test_context.rs +++ b/crypto/src/test_utils/test_context.rs @@ -66,6 +66,114 @@ pub use crate::{CipherSuite, ConversationConfiguration, CredentialType, WirePoli crate::CredentialType::X509, openmls::prelude::Ciphersuite::MLS_256_DHKEMP384_AES256GCM_SHA384_P384 )), + // Post-quantum variants. + case::basic_f001(TestContext::new( + crate::CredentialType::Basic, + openmls::prelude::Ciphersuite::MLS_128_MLKEM768X25519_AES128GCM_SHA256_Ed25519 + )), + case::cert_f001(TestContext::new( + crate::CredentialType::X509, + openmls::prelude::Ciphersuite::MLS_128_MLKEM768X25519_AES128GCM_SHA256_Ed25519 + )), + #[cfg(feature = "test-all-cipher")] + case::basic_f002(TestContext::new( + crate::CredentialType::Basic, + openmls::prelude::Ciphersuite::MLS_128_MLKEM768X25519_AES256GCM_SHA384_Ed25519, + )), + #[cfg(feature = "test-all-cipher")] + case::cert_f002(TestContext::new( + crate::CredentialType::X509, + openmls::prelude::Ciphersuite::MLS_128_MLKEM768X25519_AES256GCM_SHA384_Ed25519, + )), + #[cfg(feature = "test-all-cipher")] + case::basic_f003(TestContext::new( + crate::CredentialType::Basic, + openmls::prelude::Ciphersuite::MLS_128_MLKEM768P256_AES128GCM_SHA256_P256, + )), + #[cfg(feature = "test-all-cipher")] + case::cert_f003(TestContext::new( + crate::CredentialType::X509, + openmls::prelude::Ciphersuite::MLS_128_MLKEM768P256_AES128GCM_SHA256_P256, + )), + #[cfg(feature = "test-all-cipher")] + case::basic_f004(TestContext::new( + crate::CredentialType::Basic, + openmls::prelude::Ciphersuite::MLS_128_MLKEM768P256_AES256GCM_SHA384_P256, + )), + #[cfg(feature = "test-all-cipher")] + case::cert_f004(TestContext::new( + crate::CredentialType::X509, + openmls::prelude::Ciphersuite::MLS_128_MLKEM768P256_AES256GCM_SHA384_P256, + )), + #[cfg(feature = "test-all-cipher")] + case::basic_f005(TestContext::new( + crate::CredentialType::Basic, + openmls::prelude::Ciphersuite::MLS_192_MLKEM1024P384_AES256GCM_SHA384_P384, + )), + #[cfg(feature = "test-all-cipher")] + case::cert_f005(TestContext::new( + crate::CredentialType::X509, + openmls::prelude::Ciphersuite::MLS_192_MLKEM1024P384_AES256GCM_SHA384_P384, + )), + #[cfg(feature = "test-all-cipher")] + case::basic_f006(TestContext::new( + crate::CredentialType::Basic, + openmls::prelude::Ciphersuite::MLS_128_MLKEM768_AES256GCM_SHA384_Ed25519, + )), + #[cfg(feature = "test-all-cipher")] + case::cert_f006(TestContext::new( + crate::CredentialType::X509, + openmls::prelude::Ciphersuite::MLS_128_MLKEM768_AES256GCM_SHA384_Ed25519, + )), + #[cfg(feature = "test-all-cipher")] + case::basic_f007(TestContext::new( + crate::CredentialType::Basic, + openmls::prelude::Ciphersuite::MLS_128_MLKEM768_AES256GCM_SHA384_P256, + )), + #[cfg(feature = "test-all-cipher")] + case::cert_f007(TestContext::new( + crate::CredentialType::X509, + openmls::prelude::Ciphersuite::MLS_128_MLKEM768_AES256GCM_SHA384_P256, + )), + #[cfg(feature = "test-all-cipher")] + case::basic_f008(TestContext::new( + crate::CredentialType::Basic, + openmls::prelude::Ciphersuite::MLS_192_MLKEM1024_AES256GCM_SHA384_P384, + )), + #[cfg(feature = "test-all-cipher")] + case::cert_f008(TestContext::new( + crate::CredentialType::X509, + openmls::prelude::Ciphersuite::MLS_192_MLKEM1024_AES256GCM_SHA384_P384, + )), + case::basic_f009(TestContext::new( + crate::CredentialType::Basic, + openmls::prelude::Ciphersuite::MLS_128_MLKEM768X25519_CHACHA20POLY1305_SHA384_MLDSA44, + )), + #[cfg(feature = "test-all-cipher")] + case::cert_f009(TestContext::new( + crate::CredentialType::X509, + openmls::prelude::Ciphersuite::MLS_128_MLKEM768X25519_CHACHA20POLY1305_SHA384_MLDSA44, + )), + #[cfg(feature = "test-all-cipher")] + case::basic_f00a(TestContext::new( + crate::CredentialType::Basic, + openmls::prelude::Ciphersuite::MLS_192_MLKEM768_AES256GCM_SHA384_MLDSA65, + )), + #[cfg(feature = "test-all-cipher")] + case::cert_f00a(TestContext::new( + crate::CredentialType::X509, + openmls::prelude::Ciphersuite::MLS_192_MLKEM768_AES256GCM_SHA384_MLDSA65, + )), + #[cfg(feature = "test-all-cipher")] + case::basic_f00b(TestContext::new( + crate::CredentialType::Basic, + openmls::prelude::Ciphersuite::MLS_256_MLKEM1024_AES256GCM_SHA384_MLDSA87, + )), + #[cfg(feature = "test-all-cipher")] + case::cert_f00b(TestContext::new( + crate::CredentialType::X509, + openmls::prelude::Ciphersuite::MLS_256_MLKEM1024_AES256GCM_SHA384_MLDSA87, + )), )] #[test_attr(macro_rules_attribute::apply(smol_macros::test))] #[allow(non_snake_case)] From 5ae31e6674f5e7096ba3c4fcc3b375ddb878d9c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Thu, 8 Oct 2026 15:57:48 +0200 Subject: [PATCH 16/17] test: crypto: heterogeneous_clients_can_send_messages cannot work yet with ML-DSA signatures Support for post-quantum signatures is not yet available for x509 credentials, so just skip it. --- crypto/src/mls/credential/mod.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crypto/src/mls/credential/mod.rs b/crypto/src/mls/credential/mod.rs index 51d33dd9dde..fa9543f075e 100644 --- a/crypto/src/mls/credential/mod.rs +++ b/crypto/src/mls/credential/mod.rs @@ -237,6 +237,17 @@ mod tests { #[apply(all_cred_cipher)] async fn heterogeneous_clients_can_send_messages(case: TestContext) { + // We don't yet support ML-DSA signatures with x509 credentials. + if [ + SignatureScheme::MLDSA44, + SignatureScheme::MLDSA65, + SignatureScheme::MLDSA87, + ] + .contains(&case.signature_scheme()) + { + return; + } + // check that both credentials can initiate/join a group let ([x509_session], [basic_session]) = case.sessions_mixed_credential_types().await; From c8ca3cf987ad96be53920baf11ed1e72b8bbe140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Stankovi=C4=87?= Date: Thu, 8 Oct 2026 16:11:27 +0200 Subject: [PATCH 17/17] doc: mention PQ ciphersuites in the release notes --- cc-book/src/release_notes.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/cc-book/src/release_notes.md b/cc-book/src/release_notes.md index e1c7b97710d..721e6113b02 100644 --- a/cc-book/src/release_notes.md +++ b/cc-book/src/release_notes.md @@ -2,6 +2,28 @@ ## Unreleased +- CoreCrypto now has support for 11 post-quantum ciphersuites, as defined by the + [draft](https://www.ietf.org/archive/id/draft-ietf-mls-pq-ciphersuites-06.html). Specifically, support has been added + for the 6 ciphersuites that use hybrid (post-quantum + classical) key-exchange mechanisms: + + - `MLS_128_MLKEM768X25519_AES128GCM_SHA256_Ed25519` + - `MLS_128_MLKEM768X25519_AES256GCM_SHA384_Ed25519` + - `MLS_128_MLKEM768P256_AES128GCM_SHA256_P256` + - `MLS_128_MLKEM768P256_AES256GCM_SHA384_P256` + - `MLS_128_MLKEM768X25519_CHACHA20POLY1305_SHA384_MLDSA44` + - `MLS_192_MLKEM1024P384_AES256GCM_SHA384_P384` + + as well as the 5 ciphersuites that use pure post-quantum key-exchange mechanisms: + + - `MLS_128_MLKEM768_AES256GCM_SHA384_Ed25519` + - `MLS_128_MLKEM768_AES256GCM_SHA384_P256` + - `MLS_192_MLKEM1024_AES256GCM_SHA384_P384` + - `MLS_192_MLKEM768_AES256GCM_SHA384_MLDSA65` + - `MLS_256_MLKEM1024_AES256GCM_SHA384_MLDSA87` + + Note that only 3 of the 11 ciphersuites use post-quantum signatures (ML-DSA), and that using x509 credentials with + post-quantum signatures is not yet supported. + - `proteusNewPrekey` is deprecated; use `proteusNewPrekeyAuto` instead. - Rendering a `DeserializedClientId` (its `toString`/`Display`) now reproduces the client id it was deserialized from.