Drop X448, P-521, FFDHE2048 to match boring's default group list

This commit is contained in:
Jan Rüth 2026-04-10 14:58:19 +02:00 committed by Jan
commit c71d5bbfd8
6 changed files with 57 additions and 244 deletions

View file

@ -42,23 +42,13 @@ ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
### Key Exchange Groups ### Key Exchange Groups
Post-quantum hybrid (requires `mlkem` feature, TLS 1.3 only): Matches boring's default supported group list:
```
X25519MLKEM768 (0x11ec)
```
ECDHE:
``` ```
X25519MLKEM768 (0x11ec, requires mlkem feature, TLS 1.3 only)
X25519 X25519
X448
secp256r1 (P-256) secp256r1 (P-256)
secp384r1 (P-384) secp384r1 (P-384)
secp521r1 (P-521)
```
FFDHE:
```
ffdhe2048
``` ```
When `mlkem` is enabled, X25519MLKEM768 is the preferred (first) group in both When `mlkem` is enabled, X25519MLKEM768 is the preferred (first) group in both
@ -108,7 +98,7 @@ boring's `fips202205` compliance policy:
- **Cipher suites**: AES-GCM only (no ChaCha20-Poly1305). - **Cipher suites**: AES-GCM only (no ChaCha20-Poly1305).
- **Key exchange groups**: X25519MLKEM768 (preferred), P-256, and P-384 only - **Key exchange groups**: X25519MLKEM768 (preferred), P-256, and P-384 only
(no standalone X25519, X448, P-521, or FFDHE). (no standalone X25519).
- **Signature algorithms**: RSA PKCS#1 / PSS and ECDSA with P-256 or P-384 only - **Signature algorithms**: RSA PKCS#1 / PSS and ECDSA with P-256 or P-384 only
(no P-521, Ed25519, or Ed448). (no P-521, Ed25519, or Ed448).

View file

@ -1,123 +0,0 @@
use boring::{dh::Dh, error::ErrorStack, pkey::Private};
use foreign_types::ForeignType;
use rustls::crypto;
use crate::helper::{cvt, cvt_p, log_and_map};
use super::DhKeyType;
/// This type can be used to perform a
/// Diffie-Hellman key exchange.
pub struct KeyExchange {
dh: Dh<Private>,
pub_bytes: Vec<u8>,
key_type: DhKeyType,
}
impl KeyExchange {
// Generate a new KeyExchange with a random FFDHE_2048 private key
pub fn generate_ffdhe_2048() -> Result<Self, ErrorStack> {
let mut me = Self {
dh: unsafe { Dh::from_ptr(cvt_p(boring_sys::DH_get_rfc7919_2048())?) },
pub_bytes: Vec::new(),
key_type: DhKeyType::FFDHE2048,
};
me.pub_bytes = unsafe {
// generate a new key pair
cvt(boring_sys::DH_generate_key(me.dh.as_ptr()))?;
// get a reference to the pub key
let pubkey = boring_sys::DH_get0_pub_key(me.dh.as_ptr());
// figure out how many bytes we need, round up to the next full byte
let size = (boring_sys::BN_num_bits(pubkey) as usize).div_ceil(8);
// alloc a vector with enough capacity
let mut v = Vec::with_capacity(size);
// convert to binary representation
let after_size = boring_sys::BN_bn2bin(pubkey, v.as_mut_ptr());
// size should be what we calculated before
assert_eq!(size, after_size);
// ensure those bytes are accessible in the vec
v.set_len(size);
v
};
Ok(me)
}
/// Generate a shared secret with the other's raw public key
fn diffie_hellman(&self, raw_public_key: &[u8]) -> Result<Vec<u8>, ErrorStack> {
let peer = boring::bn::BigNum::from_slice(raw_public_key)?;
let secret_len = unsafe { cvt(boring_sys::DH_size(self.dh.as_ptr()))? } as usize;
let mut secret = vec![0u8; secret_len];
let secret_len = unsafe {
cvt(boring_sys::DH_compute_key_padded(
secret.as_mut_ptr(),
peer.as_ptr(),
self.dh.as_ptr(),
))?
} as usize;
secret.truncate(secret_len);
Ok(secret)
}
}
impl crypto::ActiveKeyExchange for KeyExchange {
fn complete(
self: Box<Self>,
peer_pub_key: &[u8],
) -> Result<crypto::SharedSecret, rustls::Error> {
let expected_len = self.pub_bytes.len();
if peer_pub_key.len() != expected_len {
return Err(rustls::Error::from(rustls::PeerMisbehaved::InvalidKeyShare));
}
Ok(crypto::SharedSecret::from(
self.diffie_hellman(peer_pub_key)
.map_err(|e| {
log_and_map(
"dh::KeyExchange::diffie_hellman",
e,
rustls::PeerMisbehaved::InvalidKeyShare,
)
})?
.as_ref(),
))
}
fn pub_key(&self) -> &[u8] {
self.pub_bytes.as_ref()
}
fn group(&self) -> rustls::NamedGroup {
match self.key_type {
DhKeyType::FFDHE2048 => rustls::NamedGroup::FFDHE2048,
_ => unimplemented!(),
}
}
}
#[cfg(test)]
mod tests {
use crate::kx::dh::KeyExchange;
use rustls::crypto::ActiveKeyExchange;
#[test]
fn test_derive_dh() {
let alice = KeyExchange::generate_ffdhe_2048().unwrap();
let bob = KeyExchange::generate_ffdhe_2048().unwrap();
let shared_secret1 = alice.diffie_hellman(bob.pub_key()).unwrap();
let shared_secret2 = bob.diffie_hellman(alice.pub_key()).unwrap();
assert_eq!(shared_secret1, shared_secret2)
}
}

View file

@ -1,7 +1,5 @@
use std::{ #[cfg(not(feature = "fips"))]
mem::MaybeUninit, use std::{mem::MaybeUninit, ptr};
ptr::{self},
};
use boring::{ use boring::{
ec::{EcGroup, EcKey}, ec::{EcGroup, EcKey},
@ -9,12 +7,16 @@ use boring::{
nid::Nid, nid::Nid,
pkey::{PKey, PKeyRef, Private}, pkey::{PKey, PKeyRef, Private},
}; };
#[cfg(not(feature = "fips"))]
use boring_additions::evp::EvpPkeyCtx; use boring_additions::evp::EvpPkeyCtx;
#[cfg(not(feature = "fips"))]
use foreign_types::ForeignType; use foreign_types::ForeignType;
use rustls::crypto; use rustls::crypto;
use spki::der::Decode; use spki::der::Decode;
use crate::helper::{cvt, cvt_p, log_and_map}; use crate::helper::log_and_map;
#[cfg(not(feature = "fips"))]
use crate::helper::{cvt, cvt_p};
use super::DhKeyType; use super::DhKeyType;
@ -30,16 +32,11 @@ pub struct KeyExchange {
impl KeyExchange { impl KeyExchange {
/// Creates a new `KeyExchange` using a random /// Creates a new `KeyExchange` using a random
/// private key for the `X25519` Edwards curve /// private key for the `X25519` Edwards curve
#[cfg(not(feature = "fips"))]
pub fn with_x25519() -> Result<Self, ErrorStack> { pub fn with_x25519() -> Result<Self, ErrorStack> {
Self::ed_from_curve(Nid::from_raw(boring_sys::NID_X25519)) Self::ed_from_curve(Nid::from_raw(boring_sys::NID_X25519))
} }
/// Creates a new `KeyExchange` using a random
/// private key for the `X448` Edwards curve
pub fn with_x448() -> Result<Self, ErrorStack> {
Self::ed_from_curve(Nid::from_raw(boring_sys::NID_X448))
}
/// Creates a new `KeyExchange` using a random /// Creates a new `KeyExchange` using a random
/// private key for `sepc256r1` curve /// private key for `sepc256r1` curve
/// Also known as `X9_62_PRIME256V1` /// Also known as `X9_62_PRIME256V1`
@ -53,12 +50,6 @@ impl KeyExchange {
Self::ec_from_curve(Nid::SECP384R1) Self::ec_from_curve(Nid::SECP384R1)
} }
/// Creates a new `KeyExchange` using a random
/// private key for `sep521r1` curve
pub fn with_secp521r1() -> Result<Self, ErrorStack> {
Self::ec_from_curve(Nid::SECP521R1)
}
/// Allows getting a new `KeyExchange` using Eliptic Curves /// Allows getting a new `KeyExchange` using Eliptic Curves
/// on the specified curve /// on the specified curve
fn ec_from_curve(nid: Nid) -> Result<Self, ErrorStack> { fn ec_from_curve(nid: Nid) -> Result<Self, ErrorStack> {
@ -76,6 +67,7 @@ impl KeyExchange {
/// Allows getting a new `KeyExchange` using Edwards Curves /// Allows getting a new `KeyExchange` using Edwards Curves
/// on the specified curve /// on the specified curve
#[cfg(not(feature = "fips"))]
fn ed_from_curve(nid: Nid) -> Result<Self, ErrorStack> { fn ed_from_curve(nid: Nid) -> Result<Self, ErrorStack> {
let pkey_ctx = unsafe { let pkey_ctx = unsafe {
EvpPkeyCtx::from_ptr(cvt_p(boring_sys::EVP_PKEY_CTX_new_id( EvpPkeyCtx::from_ptr(cvt_p(boring_sys::EVP_PKEY_CTX_new_id(
@ -129,8 +121,8 @@ impl KeyExchange {
crate::verify::ec::create_public_key(group, point.as_ref())? crate::verify::ec::create_public_key(group, point.as_ref())?
} }
#[cfg(not(feature = "fips"))]
DhKeyType::ED(nid) => crate::verify::ed::public_key(peer_pub_key, Nid::from_raw(*nid))?, DhKeyType::ED(nid) => crate::verify::ed::public_key(peer_pub_key, Nid::from_raw(*nid))?,
_ => unimplemented!(),
}; };
let mut deriver = boring::derive::Deriver::new(&self.own_key)?; let mut deriver = boring::derive::Deriver::new(&self.own_key)?;
@ -163,12 +155,11 @@ impl crypto::ActiveKeyExchange for KeyExchange {
fn group(&self) -> rustls::NamedGroup { fn group(&self) -> rustls::NamedGroup {
match self.key_type { match self.key_type {
#[cfg(not(feature = "fips"))]
DhKeyType::ED(boring_sys::NID_X25519) => rustls::NamedGroup::X25519, DhKeyType::ED(boring_sys::NID_X25519) => rustls::NamedGroup::X25519,
DhKeyType::ED(boring_sys::NID_X448) => rustls::NamedGroup::X448,
DhKeyType::EC((_, boring_sys::NID_X9_62_prime256v1)) => rustls::NamedGroup::secp256r1, DhKeyType::EC((_, boring_sys::NID_X9_62_prime256v1)) => rustls::NamedGroup::secp256r1,
DhKeyType::EC((_, boring_sys::NID_secp384r1)) => rustls::NamedGroup::secp384r1, DhKeyType::EC((_, boring_sys::NID_secp384r1)) => rustls::NamedGroup::secp384r1,
DhKeyType::EC((_, boring_sys::NID_secp521r1)) => rustls::NamedGroup::secp521r1, _ => unreachable!("unsupported key type"),
_ => unimplemented!(),
} }
} }
} }
@ -190,6 +181,7 @@ mod tests {
} }
#[test] #[test]
#[cfg(not(feature = "fips"))]
fn test_derive_ed() { fn test_derive_ed() {
let alice = Box::new(KeyExchange::with_x25519().unwrap()); let alice = Box::new(KeyExchange::with_x25519().unwrap());
let bob = KeyExchange::with_x25519().unwrap(); let bob = KeyExchange::with_x25519().unwrap();

View file

@ -2,23 +2,26 @@ use rustls::crypto::{self, ActiveKeyExchange};
use crate::helper::log_and_map; use crate::helper::log_and_map;
mod dh;
mod ex; mod ex;
#[cfg(feature = "mlkem")] #[cfg(feature = "mlkem")]
mod pq; mod pq;
#[cfg(feature = "mlkem")] #[cfg(feature = "mlkem")]
pub(crate) use pq::X25519MlKem768; pub(crate) use pq::X25519MlKem768;
/// Key type discriminant used by [`ex::KeyExchange`] to select the
/// appropriate peer key parsing and DH derivation logic.
enum DhKeyType { enum DhKeyType {
EC((boring::ec::EcGroup, i32)), EC((boring::ec::EcGroup, i32)),
#[cfg(not(feature = "fips"))]
ED(i32), ED(i32),
FFDHE2048,
} }
/// A X25519-based key exchange /// A X25519-based key exchange
#[cfg(not(feature = "fips"))]
#[derive(Debug)] #[derive(Debug)]
pub struct X25519; pub struct X25519;
#[cfg(not(feature = "fips"))]
impl crypto::SupportedKxGroup for X25519 { impl crypto::SupportedKxGroup for X25519 {
fn start(&self) -> Result<Box<dyn ActiveKeyExchange + 'static>, rustls::Error> { fn start(&self) -> Result<Box<dyn ActiveKeyExchange + 'static>, rustls::Error> {
Ok(Box::new(ex::KeyExchange::with_x25519().map_err(|e| { Ok(Box::new(ex::KeyExchange::with_x25519().map_err(|e| {
@ -31,22 +34,6 @@ impl crypto::SupportedKxGroup for X25519 {
} }
} }
/// A X448-based key exchange
#[derive(Debug)]
pub struct X448;
impl crypto::SupportedKxGroup for X448 {
fn start(&self) -> Result<Box<dyn ActiveKeyExchange + 'static>, rustls::Error> {
Ok(Box::new(ex::KeyExchange::with_x448().map_err(|e| {
log_and_map("X448.start", e, crypto::GetRandomFailed)
})?))
}
fn name(&self) -> rustls::NamedGroup {
rustls::NamedGroup::X448
}
}
/// A secp256r1-based key exchange /// A secp256r1-based key exchange
#[derive(Debug)] #[derive(Debug)]
pub struct Secp256r1; pub struct Secp256r1;
@ -78,35 +65,3 @@ impl crypto::SupportedKxGroup for Secp384r1 {
rustls::NamedGroup::secp384r1 rustls::NamedGroup::secp384r1
} }
} }
/// A secp521r1-based key exchange
#[derive(Debug)]
pub struct Secp521r1;
impl crypto::SupportedKxGroup for Secp521r1 {
fn start(&self) -> Result<Box<dyn ActiveKeyExchange + 'static>, rustls::Error> {
Ok(Box::new(ex::KeyExchange::with_secp521r1().map_err(
|e| log_and_map("Secp521r1.start", e, crypto::GetRandomFailed),
)?))
}
fn name(&self) -> rustls::NamedGroup {
rustls::NamedGroup::secp521r1
}
}
/// A ffedhe2048-based key exchange
#[derive(Debug)]
pub struct FfDHe2048;
impl crypto::SupportedKxGroup for FfDHe2048 {
fn start(&self) -> Result<Box<dyn ActiveKeyExchange + 'static>, rustls::Error> {
Ok(Box::new(dh::KeyExchange::generate_ffdhe_2048().map_err(
|e| log_and_map("FfDHe2048.start", e, crypto::GetRandomFailed),
)?))
}
fn name(&self) -> rustls::NamedGroup {
rustls::NamedGroup::FFDHE2048
}
}

View file

@ -66,7 +66,7 @@ impl rustls::crypto::KeyProvider for Provider {
} }
} }
#[allow(unused)] #[cfg(feature = "fips")]
static ALL_FIPS_CIPHER_SUITES: &[SupportedCipherSuite] = &[ static ALL_FIPS_CIPHER_SUITES: &[SupportedCipherSuite] = &[
SupportedCipherSuite::Tls13(&tls13::AES_256_GCM_SHA384), SupportedCipherSuite::Tls13(&tls13::AES_256_GCM_SHA384),
SupportedCipherSuite::Tls13(&tls13::AES_128_GCM_SHA256), SupportedCipherSuite::Tls13(&tls13::AES_128_GCM_SHA256),
@ -80,7 +80,7 @@ static ALL_FIPS_CIPHER_SUITES: &[SupportedCipherSuite] = &[
SupportedCipherSuite::Tls12(&tls12::ECDHE_RSA_AES128_GCM_SHA256), SupportedCipherSuite::Tls12(&tls12::ECDHE_RSA_AES128_GCM_SHA256),
]; ];
#[allow(unused)] #[cfg(not(feature = "fips"))]
static ALL_CIPHER_SUITES: &[SupportedCipherSuite] = &[ static ALL_CIPHER_SUITES: &[SupportedCipherSuite] = &[
SupportedCipherSuite::Tls13(&tls13::CHACHA20_POLY1305_SHA256), SupportedCipherSuite::Tls13(&tls13::CHACHA20_POLY1305_SHA256),
SupportedCipherSuite::Tls13(&tls13::AES_256_GCM_SHA384), SupportedCipherSuite::Tls13(&tls13::AES_256_GCM_SHA384),
@ -102,47 +102,28 @@ static ALL_CIPHER_SUITES: &[SupportedCipherSuite] = &[
/// Allowed KX groups for FIPS per [SP 800-52r2](https://doi.org/10.6028/NIST.SP.800-52r2), /// Allowed KX groups for FIPS per [SP 800-52r2](https://doi.org/10.6028/NIST.SP.800-52r2),
/// aligned with boring's `fips202205` compliance policy. /// aligned with boring's `fips202205` compliance policy.
/// ///
/// X25519MLKEM768 is preferred when the `mlkem` feature is enabled. /// The `fips` feature implies `mlkem`, so X25519MLKEM768 is always
/// The `fips` feature implies `mlkem`, so the PQ hybrid is always /// available and preferred in FIPS mode.
/// available in FIPS mode. #[cfg(feature = "fips")]
#[cfg(feature = "mlkem")]
#[allow(unused)]
static ALL_FIPS_KX_GROUPS: &[&dyn SupportedKxGroup] = &[ static ALL_FIPS_KX_GROUPS: &[&dyn SupportedKxGroup] = &[
&kx::X25519MlKem768 as _, // PQ hybrid preferred &kx::X25519MlKem768 as _, // PQ hybrid preferred
&kx::Secp256r1 as _, // P-256 &kx::Secp256r1 as _, // P-256
&kx::Secp384r1 as _, // P-384 &kx::Secp384r1 as _, // P-384
]; ];
/// See [`ALL_FIPS_KX_GROUPS`] (mlkem variant).
#[cfg(not(feature = "mlkem"))]
#[allow(unused)]
static ALL_FIPS_KX_GROUPS: &[&dyn SupportedKxGroup] = &[
&kx::Secp256r1 as _, // P-256
&kx::Secp384r1 as _, // P-384
];
/// All supported KX groups, ordered by preference. /// All supported KX groups, ordered by preference.
/// Matches boring's default group preference order. ///
#[cfg(feature = "mlkem")] /// Matches boring's default supported group list exactly:
#[allow(unused)] /// X25519MLKEM768 (when mlkem enabled), X25519, P-256, P-384.
#[cfg(all(not(feature = "fips"), feature = "mlkem"))]
static ALL_KX_GROUPS: &[&dyn SupportedKxGroup] = &[ static ALL_KX_GROUPS: &[&dyn SupportedKxGroup] = &[
&kx::X25519MlKem768 as _, // PQ hybrid preferred &kx::X25519MlKem768 as _, // PQ hybrid preferred
&kx::X25519 as _, &kx::X25519 as _,
&kx::X448 as _,
&kx::Secp256r1 as _, &kx::Secp256r1 as _,
&kx::Secp384r1 as _, &kx::Secp384r1 as _,
&kx::Secp521r1 as _,
&kx::FfDHe2048 as _,
]; ];
/// See [`ALL_KX_GROUPS`] (mlkem variant). /// See [`ALL_KX_GROUPS`] (mlkem variant).
#[cfg(not(feature = "mlkem"))] #[cfg(not(any(feature = "fips", feature = "mlkem")))]
#[allow(unused)] static ALL_KX_GROUPS: &[&dyn SupportedKxGroup] =
static ALL_KX_GROUPS: &[&dyn SupportedKxGroup] = &[ &[&kx::X25519 as _, &kx::Secp256r1 as _, &kx::Secp384r1 as _];
&kx::X25519 as _,
&kx::Secp256r1 as _,
&kx::Secp384r1 as _,
&kx::Secp521r1 as _,
&kx::X448 as _,
&kx::FfDHe2048 as _,
];

View file

@ -22,7 +22,13 @@ async fn test_tls13_crypto() {
let root_store = pki.client_root_store(); let root_store = pki.client_root_store();
let server_config = pki.server_config(); let server_config = pki.server_config();
let ciphers = [ #[cfg(feature = "fips")]
let ciphers = vec![
SupportedCipherSuite::Tls13(&tls13::AES_128_GCM_SHA256),
SupportedCipherSuite::Tls13(&tls13::AES_256_GCM_SHA384),
];
#[cfg(not(feature = "fips"))]
let ciphers = vec![
SupportedCipherSuite::Tls13(&tls13::AES_128_GCM_SHA256), SupportedCipherSuite::Tls13(&tls13::AES_128_GCM_SHA256),
SupportedCipherSuite::Tls13(&tls13::AES_256_GCM_SHA384), SupportedCipherSuite::Tls13(&tls13::AES_256_GCM_SHA384),
SupportedCipherSuite::Tls13(&tls13::CHACHA20_POLY1305_SHA256), SupportedCipherSuite::Tls13(&tls13::CHACHA20_POLY1305_SHA256),
@ -324,12 +330,12 @@ fn non_fips_provider_includes_pq_group() {
assert_eq!( assert_eq!(
groups[1], groups[1],
NamedGroup::X25519, NamedGroup::X25519,
"X25519 should remain the first classical fallback" "X25519 should be the first classical fallback"
); );
assert_eq!( assert_eq!(
groups[2], groups[2],
NamedGroup::X448, NamedGroup::secp256r1,
"X448 should remain ahead of NIST P-curves in non-FIPS mode" "P-256 should follow X25519, matching boring's default order"
); );
} }
@ -341,7 +347,13 @@ async fn test_tls12_ec_crypto() {
let root_store = pki.client_root_store(); let root_store = pki.client_root_store();
let server_config = pki.server_config(); let server_config = pki.server_config();
let ciphers = [ #[cfg(feature = "fips")]
let ciphers = vec![
SupportedCipherSuite::Tls12(&tls12::ECDHE_ECDSA_AES128_GCM_SHA256),
SupportedCipherSuite::Tls12(&tls12::ECDHE_ECDSA_AES256_GCM_SHA384),
];
#[cfg(not(feature = "fips"))]
let ciphers = vec![
SupportedCipherSuite::Tls12(&tls12::ECDHE_ECDSA_AES128_GCM_SHA256), SupportedCipherSuite::Tls12(&tls12::ECDHE_ECDSA_AES128_GCM_SHA256),
SupportedCipherSuite::Tls12(&tls12::ECDHE_ECDSA_AES256_GCM_SHA384), SupportedCipherSuite::Tls12(&tls12::ECDHE_ECDSA_AES256_GCM_SHA384),
SupportedCipherSuite::Tls12(&tls12::ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256), SupportedCipherSuite::Tls12(&tls12::ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256),
@ -368,7 +380,13 @@ async fn test_tls12_rsa_crypto() {
let root_store = pki.client_root_store(); let root_store = pki.client_root_store();
let server_config = pki.server_config(); let server_config = pki.server_config();
let ciphers = [ #[cfg(feature = "fips")]
let ciphers = vec![
SupportedCipherSuite::Tls12(&tls12::ECDHE_RSA_AES128_GCM_SHA256),
SupportedCipherSuite::Tls12(&tls12::ECDHE_RSA_AES256_GCM_SHA384),
];
#[cfg(not(feature = "fips"))]
let ciphers = vec![
SupportedCipherSuite::Tls12(&tls12::ECDHE_RSA_AES128_GCM_SHA256), SupportedCipherSuite::Tls12(&tls12::ECDHE_RSA_AES128_GCM_SHA256),
SupportedCipherSuite::Tls12(&tls12::ECDHE_RSA_AES256_GCM_SHA384), SupportedCipherSuite::Tls12(&tls12::ECDHE_RSA_AES256_GCM_SHA384),
SupportedCipherSuite::Tls12(&tls12::ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256), SupportedCipherSuite::Tls12(&tls12::ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256),