From b1188e2ef19b08c67f805029db0a4d65136c86ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20R=C3=BCth?= Date: Sun, 3 Dec 2023 22:40:52 +0100 Subject: [PATCH 01/10] Update to v22 --- Cargo.toml | 12 ++--- boring-rustls-provider/src/lib.rs | 69 +++++++++++++--------------- boring-rustls-provider/src/prf.rs | 2 +- boring-rustls-provider/src/tls12.rs | 12 ++--- boring-rustls-provider/src/tls13.rs | 6 +-- boring-rustls-provider/src/verify.rs | 2 +- boring-rustls-provider/tests/e2e.rs | 56 +++++++++++----------- examples/src/bin/client.rs | 12 ++--- 8 files changed, 83 insertions(+), 88 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0fd565e..dfc9608 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,9 +19,9 @@ resolver = "2" [workspace.dependencies] boring = { version = "4", default-features = false } boring-sys = { version = "4", default-features = false } -rustls = { version = "=0.22.0-alpha.6", default-features = false } -rustls-pemfile = { version = "=2.0.0-alpha.2" } -rustls-pki-types = { version = "0.2.3" } -tokio-rustls = { version = "0.25.0-alpha.4" } -webpki = { package = "rustls-webpki", version = "0.102.0-alpha.7", default-features = false } -webpki-roots = { version = "=0.26.0-alpha.2" } +rustls = { version = "0.22", default-features = false } +rustls-pemfile = { version = "2" } +rustls-pki-types = { version = "1" } +tokio-rustls = { version = "0.25" } +webpki = { package = "rustls-webpki", version = "0.102", default-features = false } +webpki-roots = { version = "0.26" } diff --git a/boring-rustls-provider/src/lib.rs b/boring-rustls-provider/src/lib.rs index 0183125..9f39914 100644 --- a/boring-rustls-provider/src/lib.rs +++ b/boring-rustls-provider/src/lib.rs @@ -21,56 +21,49 @@ pub mod tls12; pub mod tls13; pub mod verify; -/// The boringssl-based Rustls Crypto provider -pub static PROVIDER: &'static dyn CryptoProvider = &Provider; +pub fn provider() -> CryptoProvider { + #[cfg(feature = "fips-only")] + { + provider_with_ciphers(ALL_FIPS_CIPHER_SUITES.to_vec()) + } + #[cfg(not(feature = "fips-only"))] + { + provider_with_ciphers(ALL_CIPHER_SUITES.to_vec()) + } +} + +pub fn provider_with_ciphers(ciphers: Vec) -> CryptoProvider { + CryptoProvider { + cipher_suites: ciphers, + #[cfg(feature = "fips-only")] + kx_groups: ALL_FIPS_KX_GROUPS.to_vec(), + #[cfg(not(feature = "fips-only"))] + kx_groups: ALL_KX_GROUPS.to_vec(), + #[cfg(feature = "fips-only")] + signature_verification_algorithms: verify::ALL_FIPS_ALGORITHMS, + #[cfg(not(feature = "fips-only"))] + signature_verification_algorithms: verify::ALL_ALGORITHMS, + secure_random: &Provider, + key_provider: &Provider, + } +} #[derive(Debug)] struct Provider; -impl CryptoProvider for Provider { - fn fill_random(&self, bytes: &mut [u8]) -> Result<(), GetRandomFailed> { +impl rustls::crypto::SecureRandom for Provider { + fn fill(&self, bytes: &mut [u8]) -> Result<(), rustls::crypto::GetRandomFailed> { boring::rand::rand_bytes(bytes).map_err(|e| log_and_map("rand_bytes", e, GetRandomFailed)) } +} - fn default_cipher_suites(&self) -> &'static [SupportedCipherSuite] { - #[cfg(feature = "fips-only")] - { - ALL_FIPS_CIPHER_SUITES - } - #[cfg(not(feature = "fips-only"))] - { - ALL_CIPHER_SUITES - } - } - - fn default_kx_groups(&self) -> &'static [&'static dyn SupportedKxGroup] { - #[cfg(feature = "fips-only")] - { - ALL_FIPS_KX_GROUPS - } - #[cfg(not(feature = "fips-only"))] - { - ALL_KX_GROUPS - } - } - +impl rustls::crypto::KeyProvider for Provider { fn load_private_key( &self, key_der: PrivateKeyDer<'static>, - ) -> Result, rustls::Error> { + ) -> Result, rustls::Error> { sign::BoringPrivateKey::try_from(key_der).map(|x| Arc::new(x) as _) } - - fn signature_verification_algorithms(&self) -> rustls::WebPkiSupportedAlgorithms { - #[cfg(feature = "fips-only")] - { - verify::ALL_FIPS_ALGORITHMS - } - #[cfg(not(feature = "fips-only"))] - { - verify::ALL_ALGORITHMS - } - } } #[allow(unused)] diff --git a/boring-rustls-provider/src/prf.rs b/boring-rustls-provider/src/prf.rs index 958992e..ca174f5 100644 --- a/boring-rustls-provider/src/prf.rs +++ b/boring-rustls-provider/src/prf.rs @@ -10,7 +10,7 @@ pub struct PrfTls1WithDigest(pub boring::nid::Nid); impl crypto::tls12::Prf for PrfTls1WithDigest { fn for_key_exchange( &self, - output: &mut [u8], + output: &mut [u8; 48], kx: Box, peer_pub_key: &[u8], label: &[u8], diff --git a/boring-rustls-provider/src/tls12.rs b/boring-rustls-provider/src/tls12.rs index faf8d12..677389e 100644 --- a/boring-rustls-provider/src/tls12.rs +++ b/boring-rustls-provider/src/tls12.rs @@ -23,7 +23,7 @@ const PRF_SHA256: prf::PrfTls1WithDigest = prf::PrfTls1WithDigest(boring::nid::N const PRF_SHA384: prf::PrfTls1WithDigest = prf::PrfTls1WithDigest(boring::nid::Nid::SHA384); pub static ECDHE_ECDSA_AES128_GCM_SHA256: Tls12CipherSuite = Tls12CipherSuite { - common: rustls::CipherSuiteCommon { + common: rustls::crypto::CipherSuiteCommon { suite: rustls::CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, hash_provider: hash::SHA256, confidentiality_limit: 1 << 23, @@ -36,7 +36,7 @@ pub static ECDHE_ECDSA_AES128_GCM_SHA256: Tls12CipherSuite = Tls12CipherSuite { }; pub static ECDHE_RSA_AES128_GCM_SHA256: Tls12CipherSuite = Tls12CipherSuite { - common: rustls::CipherSuiteCommon { + common: rustls::crypto::CipherSuiteCommon { suite: rustls::CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, hash_provider: hash::SHA256, confidentiality_limit: 1 << 23, @@ -49,7 +49,7 @@ pub static ECDHE_RSA_AES128_GCM_SHA256: Tls12CipherSuite = Tls12CipherSuite { }; pub static ECDHE_ECDSA_AES256_GCM_SHA384: Tls12CipherSuite = Tls12CipherSuite { - common: rustls::CipherSuiteCommon { + common: rustls::crypto::CipherSuiteCommon { suite: rustls::CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, hash_provider: hash::SHA384, confidentiality_limit: 1 << 23, @@ -62,7 +62,7 @@ pub static ECDHE_ECDSA_AES256_GCM_SHA384: Tls12CipherSuite = Tls12CipherSuite { }; pub static ECDHE_RSA_AES256_GCM_SHA384: Tls12CipherSuite = Tls12CipherSuite { - common: rustls::CipherSuiteCommon { + common: rustls::crypto::CipherSuiteCommon { suite: rustls::CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, hash_provider: hash::SHA384, confidentiality_limit: 1 << 23, @@ -75,7 +75,7 @@ pub static ECDHE_RSA_AES256_GCM_SHA384: Tls12CipherSuite = Tls12CipherSuite { }; pub static ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256: Tls12CipherSuite = Tls12CipherSuite { - common: rustls::CipherSuiteCommon { + common: rustls::crypto::CipherSuiteCommon { suite: rustls::CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, hash_provider: hash::SHA256, confidentiality_limit: u64::MAX, @@ -88,7 +88,7 @@ pub static ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256: Tls12CipherSuite = Tls12Ci }; pub static ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256: Tls12CipherSuite = Tls12CipherSuite { - common: rustls::CipherSuiteCommon { + common: rustls::crypto::CipherSuiteCommon { suite: rustls::CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, hash_provider: hash::SHA256, confidentiality_limit: u64::MAX, diff --git a/boring-rustls-provider/src/tls13.rs b/boring-rustls-provider/src/tls13.rs index 0769e5e..81bdc23 100644 --- a/boring-rustls-provider/src/tls13.rs +++ b/boring-rustls-provider/src/tls13.rs @@ -3,7 +3,7 @@ use rustls::Tls13CipherSuite; use crate::{aead, hash, hkdf}; pub static AES_128_GCM_SHA256: Tls13CipherSuite = Tls13CipherSuite { - common: rustls::CipherSuiteCommon { + common: rustls::crypto::CipherSuiteCommon { suite: rustls::CipherSuite::TLS13_AES_128_GCM_SHA256, hash_provider: hash::SHA256, confidentiality_limit: 1 << 23, @@ -15,7 +15,7 @@ pub static AES_128_GCM_SHA256: Tls13CipherSuite = Tls13CipherSuite { }; pub static AES_256_GCM_SHA384: Tls13CipherSuite = Tls13CipherSuite { - common: rustls::CipherSuiteCommon { + common: rustls::crypto::CipherSuiteCommon { suite: rustls::CipherSuite::TLS13_AES_256_GCM_SHA384, hash_provider: hash::SHA384, confidentiality_limit: 1 << 23, @@ -27,7 +27,7 @@ pub static AES_256_GCM_SHA384: Tls13CipherSuite = Tls13CipherSuite { }; pub static CHACHA20_POLY1305_SHA256: Tls13CipherSuite = Tls13CipherSuite { - common: rustls::CipherSuiteCommon { + common: rustls::crypto::CipherSuiteCommon { suite: rustls::CipherSuite::TLS13_CHACHA20_POLY1305_SHA256, hash_provider: hash::SHA256, confidentiality_limit: u64::MAX, diff --git a/boring-rustls-provider/src/verify.rs b/boring-rustls-provider/src/verify.rs index 5de9140..15eaea0 100644 --- a/boring-rustls-provider/src/verify.rs +++ b/boring-rustls-provider/src/verify.rs @@ -1,4 +1,4 @@ -use rustls::{SignatureScheme, WebPkiSupportedAlgorithms}; +use rustls::{crypto::WebPkiSupportedAlgorithms, SignatureScheme}; pub(crate) mod ec; pub(crate) mod ed; diff --git a/boring-rustls-provider/tests/e2e.rs b/boring-rustls-provider/tests/e2e.rs index 212f1ca..dcba7f3 100644 --- a/boring-rustls-provider/tests/e2e.rs +++ b/boring-rustls-provider/tests/e2e.rs @@ -5,7 +5,7 @@ use tokio::{ net::TcpStream, }; -use boring_rustls_provider::{tls12, tls13, PROVIDER}; +use boring_rustls_provider::{tls12, tls13}; use rustls::{ version::{TLS12, TLS13}, ClientConfig, ServerConfig, SupportedCipherSuite, @@ -28,13 +28,13 @@ async fn test_tls13_crypto() { ]; for cipher in ciphers { - let config = ClientConfig::builder_with_provider(PROVIDER) - .with_cipher_suites(&[cipher]) - .with_safe_default_kx_groups() - .with_protocol_versions(&[&TLS13]) - .unwrap() - .with_root_certificates(root_store.clone()) - .with_no_client_auth(); + let config = ClientConfig::builder_with_provider(Arc::new( + boring_rustls_provider::provider_with_ciphers([cipher].to_vec()), + )) + .with_protocol_versions(&[&TLS13]) + .unwrap() + .with_root_certificates(root_store.clone()) + .with_no_client_auth(); do_exchange(config, server_config.clone()).await; } @@ -54,13 +54,13 @@ async fn test_tls12_ec_crypto() { ]; for cipher in ciphers { - let config = ClientConfig::builder_with_provider(PROVIDER) - .with_cipher_suites(&[cipher]) - .with_safe_default_kx_groups() - .with_protocol_versions(&[&TLS12]) - .unwrap() - .with_root_certificates(root_store.clone()) - .with_no_client_auth(); + let config = ClientConfig::builder_with_provider(Arc::new( + boring_rustls_provider::provider_with_ciphers([cipher].to_vec()), + )) + .with_protocol_versions(&[&TLS12]) + .unwrap() + .with_root_certificates(root_store.clone()) + .with_no_client_auth(); do_exchange(config, server_config.clone()).await; } @@ -80,13 +80,13 @@ async fn test_tls12_rsa_crypto() { ]; for cipher in ciphers { - let config = ClientConfig::builder_with_provider(PROVIDER) - .with_cipher_suites(&[cipher]) - .with_safe_default_kx_groups() - .with_protocol_versions(&[&TLS12]) - .unwrap() - .with_root_certificates(root_store.clone()) - .with_no_client_auth(); + let config = ClientConfig::builder_with_provider(Arc::new( + boring_rustls_provider::provider_with_ciphers([cipher].to_vec()), + )) + .with_protocol_versions(&[&TLS12]) + .unwrap() + .with_root_certificates(root_store.clone()) + .with_no_client_auth(); do_exchange(config, server_config.clone()).await; } @@ -176,11 +176,13 @@ impl TestPki { } fn server_config(self) -> Arc { - let mut server_config = ServerConfig::builder_with_provider(PROVIDER) - .with_safe_defaults() - .with_no_client_auth() - .with_single_cert(vec![self.server_cert_der], self.server_key_der) - .unwrap(); + let mut server_config = + ServerConfig::builder_with_provider(Arc::new(boring_rustls_provider::provider())) + .with_protocol_versions(&[&TLS12, &TLS13]) + .unwrap() + .with_no_client_auth() + .with_single_cert(vec![self.server_cert_der], self.server_key_der) + .unwrap(); server_config.key_log = Arc::new(rustls::KeyLogFile::new()); diff --git a/examples/src/bin/client.rs b/examples/src/bin/client.rs index 55a4cc0..f1780c1 100644 --- a/examples/src/bin/client.rs +++ b/examples/src/bin/client.rs @@ -2,18 +2,18 @@ use std::io::{stdout, Read, Write}; use std::net::TcpStream; use std::sync::Arc; -use boring_rustls_provider::PROVIDER; - fn main() { env_logger::init(); let mut root_store = rustls::RootCertStore::empty(); root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); - let config = rustls::ClientConfig::builder_with_provider(PROVIDER) - .with_safe_defaults() - .with_root_certificates(root_store) - .with_no_client_auth(); + let config = + rustls::ClientConfig::builder_with_provider(boring_rustls_provider::provider().into()) + .with_safe_default_protocol_versions() + .unwrap() + .with_root_certificates(root_store) + .with_no_client_auth(); let server_name = "www.rust-lang.org".try_into().unwrap(); let mut conn = rustls::ClientConnection::new(Arc::new(config), server_name).unwrap(); From 03b48134ca0582ddaea9488eee476aceaa3a444e Mon Sep 17 00:00:00 2001 From: John Howard Date: Tue, 20 Feb 2024 17:39:00 -0800 Subject: [PATCH 02/10] Fix QUIC compilation and FIPS features Fix a build issue due to lack of trait constraints. Pass FIPS feature flag through and test it --- boring-rustls-provider/Cargo.toml | 3 +-- boring-rustls-provider/src/aead.rs | 2 +- boring-rustls-provider/src/helper.rs | 2 +- boring-rustls-provider/tests/e2e.rs | 12 ++++++++++++ 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/boring-rustls-provider/Cargo.toml b/boring-rustls-provider/Cargo.toml index 53ac217..8b60daf 100644 --- a/boring-rustls-provider/Cargo.toml +++ b/boring-rustls-provider/Cargo.toml @@ -12,10 +12,9 @@ default = ["tls12"] # Use a FIPS-validated version of boringssl. fips = ["boring/fips", "boring-sys/fips"] logging = ["log"] -fips-only = [] +fips-only = ["boring/fips", "boring-sys/fips"] tls12 = ["rustls/tls12"] - [dependencies] aead = {version = "0.5", default_features = false, features = ["alloc"] } boring = { workspace = true } diff --git a/boring-rustls-provider/src/aead.rs b/boring-rustls-provider/src/aead.rs index cf93908..800b487 100644 --- a/boring-rustls-provider/src/aead.rs +++ b/boring-rustls-provider/src/aead.rs @@ -32,7 +32,7 @@ pub(crate) trait BoringCipher { fn extract_keys(key: cipher::AeadKey, iv: cipher::Iv) -> ConnectionTrafficSecrets; } -pub(crate) trait QuicCipher { +pub(crate) trait QuicCipher: Send + Sync { /// The key size in bytes const KEY_SIZE: usize; diff --git a/boring-rustls-provider/src/helper.rs b/boring-rustls-provider/src/helper.rs index 6d7f173..b632357 100644 --- a/boring-rustls-provider/src/helper.rs +++ b/boring-rustls-provider/src/helper.rs @@ -38,6 +38,6 @@ pub(crate) fn log_and_map(func: &'static str, e: E, ma } #[cfg(not(feature = "log"))] -pub(crate) fn log_and_map(func: &'static str, e: E, mapped: T) -> T { +pub(crate) fn log_and_map(_func: &'static str, _e: E, mapped: T) -> T { mapped } diff --git a/boring-rustls-provider/tests/e2e.rs b/boring-rustls-provider/tests/e2e.rs index dcba7f3..21b017a 100644 --- a/boring-rustls-provider/tests/e2e.rs +++ b/boring-rustls-provider/tests/e2e.rs @@ -40,6 +40,18 @@ async fn test_tls13_crypto() { } } +#[test] +#[cfg(any(feature = "fips", feature = "fips-only"))] +fn is_fips_enabled() { + assert!(boring::fips::enabled()); +} + +#[test] +#[cfg(not(any(feature = "fips", feature = "fips-only")))] +fn is_fips_disabled() { + assert!(!boring::fips::enabled()); +} + #[tokio::test] async fn test_tls12_ec_crypto() { let pki = TestPki::new(&rcgen::PKCS_ECDSA_P256_SHA256); From aa6e1c36f880002ceb56f99a64d19e0503e0bec7 Mon Sep 17 00:00:00 2001 From: John Howard Date: Tue, 26 Mar 2024 15:37:00 -0700 Subject: [PATCH 03/10] Update to rustls 0.23 --- Cargo.toml | 4 +- boring-rustls-provider/src/aead.rs | 136 +++++++++++++++----- boring-rustls-provider/src/aead/aes.rs | 6 + boring-rustls-provider/src/aead/chacha20.rs | 3 + boring-rustls-provider/src/tls12.rs | 6 - boring-rustls-provider/src/tls13.rs | 3 - examples/Cargo.toml | 2 +- 7 files changed, 116 insertions(+), 44 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index dfc9608..5bda8df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,9 +19,9 @@ resolver = "2" [workspace.dependencies] boring = { version = "4", default-features = false } boring-sys = { version = "4", default-features = false } -rustls = { version = "0.22", default-features = false } +rustls = { version = "0.23", default-features = false } rustls-pemfile = { version = "2" } rustls-pki-types = { version = "1" } -tokio-rustls = { version = "0.25" } +tokio-rustls = { version = "0.26", default-features = false } webpki = { package = "rustls-webpki", version = "0.102", default-features = false } webpki-roots = { version = "0.26" } diff --git a/boring-rustls-provider/src/aead.rs b/boring-rustls-provider/src/aead.rs index 800b487..b3cc1ba 100644 --- a/boring-rustls-provider/src/aead.rs +++ b/boring-rustls-provider/src/aead.rs @@ -1,9 +1,11 @@ use std::marker::PhantomData; -use aead::{AeadCore, AeadInPlace, Nonce, Tag}; +use aead::{AeadCore, AeadInPlace, Buffer, Nonce, Tag}; use boring::error::ErrorStack; use boring_additions::aead::Algorithm; -use rustls::crypto::cipher::{self, make_tls12_aad, make_tls13_aad, Iv}; +use rustls::crypto::cipher::{ + self, make_tls12_aad, make_tls13_aad, BorrowedPayload, Iv, PrefixedPayload, +}; use rustls::{ConnectionTrafficSecrets, ContentType, ProtocolVersion}; use crate::helper::log_and_map; @@ -25,6 +27,12 @@ pub(crate) trait BoringCipher { /// The length of the authentication tag const TAG_LEN: usize; + /// integrity limit + const INTEGRITY_LIMIT: u64; + + /// confidentiality limit + const CONFIDENTIALITY_LIMIT: u64; + /// Constructs a new instance of this cipher as an AEAD algorithm fn new_cipher() -> Algorithm; @@ -123,11 +131,10 @@ where { fn encrypt( &mut self, - msg: cipher::BorrowedPlainMessage, + msg: cipher::OutboundPlainMessage, seq: u64, - ) -> Result { + ) -> Result { let nonce = cipher::Nonce::new(&self.iv, seq); - match self.tls_version { #[cfg(feature = "tls12")] ProtocolVersion::TLSv1_2 => { @@ -136,37 +143,41 @@ where let total_len = self.encrypted_payload_len(msg.payload.len()); - let mut full_payload = Vec::with_capacity(total_len); + let mut full_payload = PrefixedPayload::with_capacity(total_len); full_payload.extend_from_slice(&nonce.0.as_ref()[fixed_iv_len..]); - full_payload.extend_from_slice(msg.payload); + full_payload.extend_from_chunks(&msg.payload); full_payload.extend_from_slice(&vec![0u8; self.crypter.max_overhead()]); - let (_, payload) = full_payload.split_at_mut(explicit_nonce_len); + let (_, payload) = full_payload.as_mut().split_at_mut(explicit_nonce_len); let (payload, tag) = payload.split_at_mut(msg.payload.len()); let aad = cipher::make_tls12_aad(seq, msg.typ, msg.version, msg.payload.len()); self.crypter .seal_in_place(&nonce.0, &aad, payload, tag) .map_err(|_| rustls::Error::EncryptError) - .map(|_| cipher::OpaqueMessage::new(msg.typ, msg.version, full_payload)) + .map(|_| cipher::OutboundOpaqueMessage::new(msg.typ, msg.version, full_payload)) } ProtocolVersion::TLSv1_3 => { let total_len = self.encrypted_payload_len(msg.payload.len()); - let mut payload = Vec::with_capacity(total_len); - payload.extend_from_slice(msg.payload); - payload.push(msg.typ.get_u8()); + let mut payload = PrefixedPayload::with_capacity(total_len); + payload.extend_from_chunks(&msg.payload); + payload.extend_from_slice(&msg.typ.to_array()); let aad = cipher::make_tls13_aad(total_len); - self.encrypt_in_place(Nonce::::from_slice(&nonce.0), &aad, &mut payload) - .map_err(|_| rustls::Error::EncryptError) - .map(|_| { - cipher::OpaqueMessage::new( - ContentType::ApplicationData, - ProtocolVersion::TLSv1_2, - payload, - ) - }) + self.encrypt_in_place( + Nonce::::from_slice(&nonce.0), + &aad, + &mut EncryptBufferAdapter(&mut payload), + ) + .map_err(|_| rustls::Error::EncryptError) + .map(|_| { + cipher::OutboundOpaqueMessage::new( + ContentType::ApplicationData, + ProtocolVersion::TLSv1_2, + payload, + ) + }) } _ => unimplemented!(), } @@ -187,11 +198,11 @@ impl cipher::MessageDecrypter for BoringAeadCrypter where T: BoringAead, { - fn decrypt( + fn decrypt<'a>( &mut self, - mut m: cipher::OpaqueMessage, + mut m: cipher::InboundOpaqueMessage<'a>, seq: u64, - ) -> Result { + ) -> Result, rustls::Error> { match self.tls_version { #[cfg(feature = "tls12")] ProtocolVersion::TLSv1_2 => { @@ -199,11 +210,11 @@ where // payload is: [nonce] | [ciphertext] | [auth tag] let actual_payload_length = - m.payload().len() - self.crypter.max_overhead() - explicit_nonce_len; + m.payload.len() - self.crypter.max_overhead() - explicit_nonce_len; let aad = make_tls12_aad(seq, m.typ, m.version, actual_payload_length); - let payload = m.payload_mut(); + let payload = &mut m.payload; // get the nonce let (explicit_nonce, payload) = payload.split_at_mut(explicit_nonce_len); @@ -230,20 +241,24 @@ where .map_err(|e| log_and_map("open_in_place", e, rustls::Error::DecryptError)) .map(|_| { // rotate the nonce to the end - m.payload_mut().rotate_left(explicit_nonce_len); + m.payload.rotate_left(explicit_nonce_len); // truncate buffer to the actual payload - m.payload_mut().truncate(actual_payload_length); + m.payload.truncate(actual_payload_length); m.into_plain_message() }) } ProtocolVersion::TLSv1_3 => { let nonce = cipher::Nonce::new(&self.iv, seq); - let aad = make_tls13_aad(m.payload().len()); - self.decrypt_in_place(Nonce::::from_slice(&nonce.0), &aad, m.payload_mut()) - .map_err(|_| rustls::Error::DecryptError) - .and_then(|_| m.into_tls13_unpadded_message()) + let aad = make_tls13_aad(m.payload.len()); + self.decrypt_in_place( + Nonce::::from_slice(&nonce.0), + &aad, + &mut DecryptBufferAdapter(&mut m.payload), + ) + .map_err(|_| rustls::Error::DecryptError) + .and_then(|_| m.into_tls13_unpadded_message()) } _ => unimplemented!(), } @@ -290,6 +305,14 @@ where fn tag_len(&self) -> usize { ::TAG_LEN } + + fn confidentiality_limit(&self) -> u64 { + ::CONFIDENTIALITY_LIMIT + } + + fn integrity_limit(&self) -> u64 { + ::INTEGRITY_LIMIT + } } pub(crate) struct Aead(PhantomData); @@ -494,6 +517,55 @@ where } } +struct DecryptBufferAdapter<'a, 'p>(&'a mut BorrowedPayload<'p>); + +impl AsRef<[u8]> for DecryptBufferAdapter<'_, '_> { + fn as_ref(&self) -> &[u8] { + self.0 + } +} + +impl AsMut<[u8]> for DecryptBufferAdapter<'_, '_> { + fn as_mut(&mut self) -> &mut [u8] { + self.0 + } +} + +impl Buffer for DecryptBufferAdapter<'_, '_> { + fn extend_from_slice(&mut self, _: &[u8]) -> aead::Result<()> { + unreachable!("not used by `AeadInPlace::decrypt_in_place`") + } + + fn truncate(&mut self, len: usize) { + self.0.truncate(len) + } +} + +struct EncryptBufferAdapter<'a>(&'a mut PrefixedPayload); + +impl AsRef<[u8]> for EncryptBufferAdapter<'_> { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl AsMut<[u8]> for EncryptBufferAdapter<'_> { + fn as_mut(&mut self) -> &mut [u8] { + self.0.as_mut() + } +} + +impl Buffer for EncryptBufferAdapter<'_> { + fn extend_from_slice(&mut self, other: &[u8]) -> aead::Result<()> { + self.0.extend_from_slice(other); + Ok(()) + } + + fn truncate(&mut self, len: usize) { + self.0.truncate(len) + } +} + #[cfg(test)] mod tests { use hex_literal::hex; diff --git a/boring-rustls-provider/src/aead/aes.rs b/boring-rustls-provider/src/aead/aes.rs index 712d6ce..6a5e2af 100644 --- a/boring-rustls-provider/src/aead/aes.rs +++ b/boring-rustls-provider/src/aead/aes.rs @@ -17,6 +17,9 @@ impl BoringCipher for Aes128 { const TAG_LEN: usize = 16; + const INTEGRITY_LIMIT: u64 = 1 << 52; + const CONFIDENTIALITY_LIMIT: u64 = 1 << 23; + fn new_cipher() -> Algorithm { Algorithm::aes_128_gcm() } @@ -58,6 +61,9 @@ impl BoringCipher for Aes256 { const TAG_LEN: usize = 16; + const INTEGRITY_LIMIT: u64 = 1 << 52; + const CONFIDENTIALITY_LIMIT: u64 = 1 << 23; + fn new_cipher() -> Algorithm { Algorithm::aes_256_gcm() } diff --git a/boring-rustls-provider/src/aead/chacha20.rs b/boring-rustls-provider/src/aead/chacha20.rs index 0681fc7..2590aab 100644 --- a/boring-rustls-provider/src/aead/chacha20.rs +++ b/boring-rustls-provider/src/aead/chacha20.rs @@ -20,6 +20,9 @@ impl BoringCipher for ChaCha20Poly1305 { const TAG_LEN: usize = 16; + const INTEGRITY_LIMIT: u64 = 1 << 36; + const CONFIDENTIALITY_LIMIT: u64 = u64::MAX; + fn new_cipher() -> Algorithm { Algorithm::chacha20_poly1305() } diff --git a/boring-rustls-provider/src/tls12.rs b/boring-rustls-provider/src/tls12.rs index 677389e..caef421 100644 --- a/boring-rustls-provider/src/tls12.rs +++ b/boring-rustls-provider/src/tls12.rs @@ -27,7 +27,6 @@ pub static ECDHE_ECDSA_AES128_GCM_SHA256: Tls12CipherSuite = Tls12CipherSuite { suite: rustls::CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, hash_provider: hash::SHA256, confidentiality_limit: 1 << 23, - integrity_limit: 1 << 52, }, aead_alg: &aead::Aead::::DEFAULT, prf_provider: &PRF_SHA256, @@ -40,7 +39,6 @@ pub static ECDHE_RSA_AES128_GCM_SHA256: Tls12CipherSuite = Tls12CipherSuite { suite: rustls::CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, hash_provider: hash::SHA256, confidentiality_limit: 1 << 23, - integrity_limit: 1 << 52, }, aead_alg: &aead::Aead::::DEFAULT, prf_provider: &PRF_SHA256, @@ -53,7 +51,6 @@ pub static ECDHE_ECDSA_AES256_GCM_SHA384: Tls12CipherSuite = Tls12CipherSuite { suite: rustls::CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, hash_provider: hash::SHA384, confidentiality_limit: 1 << 23, - integrity_limit: 1 << 52, }, aead_alg: &aead::Aead::::DEFAULT, prf_provider: &PRF_SHA384, @@ -66,7 +63,6 @@ pub static ECDHE_RSA_AES256_GCM_SHA384: Tls12CipherSuite = Tls12CipherSuite { suite: rustls::CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, hash_provider: hash::SHA384, confidentiality_limit: 1 << 23, - integrity_limit: 1 << 52, }, aead_alg: &aead::Aead::::DEFAULT, prf_provider: &PRF_SHA384, @@ -79,7 +75,6 @@ pub static ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256: Tls12CipherSuite = Tls12Ci suite: rustls::CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, hash_provider: hash::SHA256, confidentiality_limit: u64::MAX, - integrity_limit: 1 << 36, }, aead_alg: &aead::Aead::::DEFAULT, prf_provider: &PRF_SHA256, @@ -92,7 +87,6 @@ pub static ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256: Tls12CipherSuite = Tls12Ciph suite: rustls::CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, hash_provider: hash::SHA256, confidentiality_limit: u64::MAX, - integrity_limit: 1 << 36, }, aead_alg: &aead::Aead::::DEFAULT, prf_provider: &PRF_SHA256, diff --git a/boring-rustls-provider/src/tls13.rs b/boring-rustls-provider/src/tls13.rs index 81bdc23..e2a45c8 100644 --- a/boring-rustls-provider/src/tls13.rs +++ b/boring-rustls-provider/src/tls13.rs @@ -7,7 +7,6 @@ pub static AES_128_GCM_SHA256: Tls13CipherSuite = Tls13CipherSuite { suite: rustls::CipherSuite::TLS13_AES_128_GCM_SHA256, hash_provider: hash::SHA256, confidentiality_limit: 1 << 23, - integrity_limit: 1 << 52, }, hkdf_provider: &hkdf::Hkdf::::DEFAULT, aead_alg: &aead::Aead::::DEFAULT, @@ -19,7 +18,6 @@ pub static AES_256_GCM_SHA384: Tls13CipherSuite = Tls13CipherSuite { suite: rustls::CipherSuite::TLS13_AES_256_GCM_SHA384, hash_provider: hash::SHA384, confidentiality_limit: 1 << 23, - integrity_limit: 1 << 52, }, hkdf_provider: &hkdf::Hkdf::::DEFAULT, aead_alg: &aead::Aead::::DEFAULT, @@ -31,7 +29,6 @@ pub static CHACHA20_POLY1305_SHA256: Tls13CipherSuite = Tls13CipherSuite { suite: rustls::CipherSuite::TLS13_CHACHA20_POLY1305_SHA256, hash_provider: hash::SHA256, confidentiality_limit: u64::MAX, - integrity_limit: 1 << 36, }, hkdf_provider: &hkdf::Hkdf::::DEFAULT, diff --git a/examples/Cargo.toml b/examples/Cargo.toml index d2e4700..9d2084d 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -13,7 +13,7 @@ log = { version = "0.4.4" } mio = { version = "0.8", features = ["net", "os-poll"] } pki-types = { package = "rustls-pki-types", version = "0.2" } rcgen = { version = "0.11.3", features = ["pem"], default-features = false } -rustls = { workspace = true, features = [ "logging" ]} +rustls = { workspace = true, features = [ "logging", "std" ]} boring-rustls-provider = { path = "../boring-rustls-provider", features = ["logging"] } rustls-pemfile = { workspace = true } serde = "1.0" From 490340afa77e2c08fc45853124f99d49f4f9f8a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20R=C3=BCth?= Date: Tue, 7 Oct 2025 18:28:23 +0200 Subject: [PATCH 04/10] Bump and cleanup deps and fix lints --- Cargo.toml | 10 +++++----- boring-additions/Cargo.toml | 1 - boring-rustls-provider/Cargo.toml | 9 +++------ boring-rustls-provider/src/kx.rs | 12 ++++++------ boring-rustls-provider/src/kx/dh.rs | 2 +- boring-rustls-provider/src/sign.rs | 6 +++--- boring-rustls-provider/src/verify/ec.rs | 12 ++++++------ boring-rustls-provider/src/verify/ed.rs | 6 +++--- boring-rustls-provider/src/verify/rsa.rs | 18 +++++++++--------- boring-sys-additions/src/lib.rs | 2 +- examples/Cargo.toml | 10 +--------- 11 files changed, 38 insertions(+), 50 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5bda8df..72a4b52 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,10 +2,10 @@ members = [ # things that should probably be in boring crate "boring-additions", - # things that should probably be in boring-sys crate - "boring-sys-additions", # the main library and tests "boring-rustls-provider", + # things that should probably be in boring-sys crate + "boring-sys-additions", # tests and example code "examples", ] @@ -22,6 +22,6 @@ boring-sys = { version = "4", default-features = false } rustls = { version = "0.23", default-features = false } rustls-pemfile = { version = "2" } rustls-pki-types = { version = "1" } -tokio-rustls = { version = "0.26", default-features = false } -webpki = { package = "rustls-webpki", version = "0.102", default-features = false } -webpki-roots = { version = "0.26" } +tokio-rustls = { version = "0.26", default-features = false } +webpki = { package = "rustls-webpki", version = "0.103", default-features = false } +webpki-roots = { version = "1.0" } diff --git a/boring-additions/Cargo.toml b/boring-additions/Cargo.toml index ef0d341..388984f 100644 --- a/boring-additions/Cargo.toml +++ b/boring-additions/Cargo.toml @@ -8,7 +8,6 @@ description = "Boring additions" publish = false [dependencies] -aead = { version = "0.5", default_features = false, features = ["alloc"] } boring = { workspace = true } boring-sys = { workspace = true } foreign-types = "0.5" diff --git a/boring-rustls-provider/Cargo.toml b/boring-rustls-provider/Cargo.toml index 8b60daf..8a02f51 100644 --- a/boring-rustls-provider/Cargo.toml +++ b/boring-rustls-provider/Cargo.toml @@ -16,22 +16,19 @@ fips-only = ["boring/fips", "boring-sys/fips"] tls12 = ["rustls/tls12"] [dependencies] -aead = {version = "0.5", default_features = false, features = ["alloc"] } +aead = {version = "0.5", default-features = false, features = ["alloc"] } boring = { workspace = true } boring-additions = { path = "../boring-additions" } boring-sys = { workspace = true } boring-sys-additions = { path = "../boring-sys-additions" } foreign-types = "0.5" -lazy_static = "1.4" log = { version = "0.4.4", optional = true } -once_cell = "1" rustls = { workspace = true } rustls-pki-types = { workspace = true } spki = "0.7" -webpki = { workspace = true, features = ["alloc", "std"] } [dev-dependencies] -hex-literal = "0.4" -rcgen = "0.11.3" +hex-literal = "1" +rcgen = "0.12" tokio = { version = "1.34", features = ["macros", "rt", "net", "io-util", "io-std"] } tokio-rustls = { workspace = true } diff --git a/boring-rustls-provider/src/kx.rs b/boring-rustls-provider/src/kx.rs index 8ec7276..0796b69 100644 --- a/boring-rustls-provider/src/kx.rs +++ b/boring-rustls-provider/src/kx.rs @@ -16,7 +16,7 @@ enum DhKeyType { pub struct X25519; impl crypto::SupportedKxGroup for X25519 { - fn start(&self) -> Result, rustls::Error> { + fn start(&self) -> Result, rustls::Error> { Ok(Box::new(ex::KeyExchange::with_x25519().map_err(|e| { log_and_map("X25519.start", e, crypto::GetRandomFailed) })?)) @@ -32,7 +32,7 @@ impl crypto::SupportedKxGroup for X25519 { pub struct X448; impl crypto::SupportedKxGroup for X448 { - fn start(&self) -> Result, rustls::Error> { + fn start(&self) -> Result, rustls::Error> { Ok(Box::new(ex::KeyExchange::with_x448().map_err(|e| { log_and_map("X448.start", e, crypto::GetRandomFailed) })?)) @@ -48,7 +48,7 @@ impl crypto::SupportedKxGroup for X448 { pub struct Secp256r1; impl crypto::SupportedKxGroup for Secp256r1 { - fn start(&self) -> Result, rustls::Error> { + fn start(&self) -> Result, rustls::Error> { Ok(Box::new(ex::KeyExchange::with_secp256r1().map_err( |e| log_and_map("Secp256r1.start", e, crypto::GetRandomFailed), )?)) @@ -64,7 +64,7 @@ impl crypto::SupportedKxGroup for Secp256r1 { pub struct Secp384r1; impl crypto::SupportedKxGroup for Secp384r1 { - fn start(&self) -> Result, rustls::Error> { + fn start(&self) -> Result, rustls::Error> { Ok(Box::new(ex::KeyExchange::with_secp384r1().map_err( |e| log_and_map("Secp384r1.start", e, crypto::GetRandomFailed), )?)) @@ -80,7 +80,7 @@ impl crypto::SupportedKxGroup for Secp384r1 { pub struct Secp521r1; impl crypto::SupportedKxGroup for Secp521r1 { - fn start(&self) -> Result, rustls::Error> { + fn start(&self) -> Result, rustls::Error> { Ok(Box::new(ex::KeyExchange::with_secp521r1().map_err( |e| log_and_map("Secp521r1.start", e, crypto::GetRandomFailed), )?)) @@ -96,7 +96,7 @@ impl crypto::SupportedKxGroup for Secp521r1 { pub struct FfDHe2048; impl crypto::SupportedKxGroup for FfDHe2048 { - fn start(&self) -> Result, rustls::Error> { + fn start(&self) -> Result, rustls::Error> { Ok(Box::new(dh::KeyExchange::generate_ffdhe_2048().map_err( |e| log_and_map("FfDHe2048.start", e, crypto::GetRandomFailed), )?)) diff --git a/boring-rustls-provider/src/kx/dh.rs b/boring-rustls-provider/src/kx/dh.rs index b004e5b..8e7839d 100644 --- a/boring-rustls-provider/src/kx/dh.rs +++ b/boring-rustls-provider/src/kx/dh.rs @@ -31,7 +31,7 @@ impl KeyExchange { 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 + 7) / 8; + 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); diff --git a/boring-rustls-provider/src/sign.rs b/boring-rustls-provider/src/sign.rs index 7817b81..41a2ead 100644 --- a/boring-rustls-provider/src/sign.rs +++ b/boring-rustls-provider/src/sign.rs @@ -63,7 +63,7 @@ fn rsa_signer_from_params( key: &PKeyRef, digest: MessageDigest, padding: Padding, -) -> Signer { +) -> Signer<'_> { let mut signer = Signer::new(digest, key).expect("failed getting signer"); signer .set_rsa_padding(padding) @@ -80,7 +80,7 @@ fn rsa_signer_from_params( signer } -fn ec_signer_from_params(key: &PKeyRef, digest: MessageDigest) -> Signer { +fn ec_signer_from_params(key: &PKeyRef, digest: MessageDigest) -> Signer<'_> { let signer = Signer::new(digest, key).expect("failed getting signer"); signer } @@ -131,7 +131,7 @@ impl SigningKey for BoringPrivateKey { pub struct BoringSigner(Arc>, rustls::SignatureScheme); impl BoringSigner { - fn get_signer(&self) -> Signer { + fn get_signer(&self) -> Signer<'_> { match self.1 { SignatureScheme::RSA_PKCS1_SHA256 => { rsa_signer_from_params(self.0.as_ref(), MessageDigest::sha256(), Padding::PKCS1) diff --git a/boring-rustls-provider/src/verify/ec.rs b/boring-rustls-provider/src/verify/ec.rs index 3de7165..f8984e3 100644 --- a/boring-rustls-provider/src/verify/ec.rs +++ b/boring-rustls-provider/src/verify/ec.rs @@ -1,5 +1,5 @@ use boring::{error::ErrorStack, hash::MessageDigest}; -use rustls::SignatureScheme; +use rustls::{pki_types::alg_id, SignatureScheme}; use rustls_pki_types::{InvalidSignature, SignatureVerificationAlgorithm}; use crate::helper; @@ -52,8 +52,8 @@ impl SignatureVerificationAlgorithm for BoringEcVerifier { fn public_key_alg_id(&self) -> rustls_pki_types::AlgorithmIdentifier { match self.0 { - SignatureScheme::ECDSA_NISTP256_SHA256 => webpki::alg_id::ECDSA_P256, - SignatureScheme::ECDSA_NISTP384_SHA384 => webpki::alg_id::ECDSA_P384, + SignatureScheme::ECDSA_NISTP256_SHA256 => alg_id::ECDSA_P256, + SignatureScheme::ECDSA_NISTP384_SHA384 => alg_id::ECDSA_P384, SignatureScheme::ECDSA_NISTP521_SHA512 => { // See rfc5480 appendix-A (secp521r1): 1.3.132.0.35 rustls_pki_types::AlgorithmIdentifier::from_slice(&[ @@ -67,8 +67,8 @@ impl SignatureVerificationAlgorithm for BoringEcVerifier { fn signature_alg_id(&self) -> rustls_pki_types::AlgorithmIdentifier { match self.0 { - SignatureScheme::ECDSA_NISTP256_SHA256 => webpki::alg_id::ECDSA_SHA256, - SignatureScheme::ECDSA_NISTP384_SHA384 => webpki::alg_id::ECDSA_SHA384, + SignatureScheme::ECDSA_NISTP256_SHA256 => alg_id::ECDSA_SHA256, + SignatureScheme::ECDSA_NISTP384_SHA384 => alg_id::ECDSA_SHA384, SignatureScheme::ECDSA_NISTP521_SHA512 => { // See rfc5480 appendix-A (ecdsa-with-SHA512): 1.2.840.10045.4.3.4 rustls_pki_types::AlgorithmIdentifier::from_slice(&[ @@ -83,7 +83,7 @@ impl SignatureVerificationAlgorithm for BoringEcVerifier { fn ec_verifier_from_params( key: &boring::pkey::PKeyRef, digest: MessageDigest, -) -> Result { +) -> Result, ErrorStack> { boring::sign::Verifier::new(digest, key) } diff --git a/boring-rustls-provider/src/verify/ed.rs b/boring-rustls-provider/src/verify/ed.rs index 5670c98..e7cb81e 100644 --- a/boring-rustls-provider/src/verify/ed.rs +++ b/boring-rustls-provider/src/verify/ed.rs @@ -2,7 +2,7 @@ use std::ptr; use boring::error::ErrorStack; use foreign_types::ForeignType; -use rustls::SignatureScheme; +use rustls::{pki_types::alg_id, SignatureScheme}; use rustls_pki_types::{InvalidSignature, SignatureVerificationAlgorithm}; use crate::helper::{cvt_p, log_and_map}; @@ -41,7 +41,7 @@ impl SignatureVerificationAlgorithm for BoringEdVerifier { fn signature_alg_id(&self) -> rustls_pki_types::AlgorithmIdentifier { match self.0 { - SignatureScheme::ED25519 => webpki::alg_id::ED25519, + SignatureScheme::ED25519 => alg_id::ED25519, SignatureScheme::ED448 => { // rfc8410#section-3: 1.3.101.113: -> DER: 06 03 2B 65 71 rustls_pki_types::AlgorithmIdentifier::from_slice(&[0x06, 0x03, 0x2B, 0x65, 0x71]) @@ -53,7 +53,7 @@ impl SignatureVerificationAlgorithm for BoringEdVerifier { fn ed_verifier_from_params( key: &boring::pkey::PKeyRef, -) -> Result { +) -> Result, ErrorStack> { boring::sign::Verifier::new_without_digest(key) } diff --git a/boring-rustls-provider/src/verify/rsa.rs b/boring-rustls-provider/src/verify/rsa.rs index 7dbfedb..5691369 100644 --- a/boring-rustls-provider/src/verify/rsa.rs +++ b/boring-rustls-provider/src/verify/rsa.rs @@ -5,7 +5,7 @@ use boring::{ rsa::{Padding, Rsa}, sign::RsaPssSaltlen, }; -use rustls::SignatureScheme; +use rustls::{pki_types::alg_id, SignatureScheme}; use rustls_pki_types::{InvalidSignature, SignatureVerificationAlgorithm}; use spki::der::Reader; @@ -73,18 +73,18 @@ impl SignatureVerificationAlgorithm for BoringRsaVerifier { } fn public_key_alg_id(&self) -> rustls_pki_types::AlgorithmIdentifier { - webpki::alg_id::RSA_ENCRYPTION + alg_id::RSA_ENCRYPTION } fn signature_alg_id(&self) -> rustls_pki_types::AlgorithmIdentifier { match self.0 { - SignatureScheme::RSA_PKCS1_SHA256 => webpki::alg_id::RSA_PKCS1_SHA256, - SignatureScheme::RSA_PKCS1_SHA384 => webpki::alg_id::RSA_PKCS1_SHA384, - SignatureScheme::RSA_PKCS1_SHA512 => webpki::alg_id::RSA_PKCS1_SHA512, + SignatureScheme::RSA_PKCS1_SHA256 => alg_id::RSA_PKCS1_SHA256, + SignatureScheme::RSA_PKCS1_SHA384 => alg_id::RSA_PKCS1_SHA384, + SignatureScheme::RSA_PKCS1_SHA512 => alg_id::RSA_PKCS1_SHA512, - SignatureScheme::RSA_PSS_SHA256 => webpki::alg_id::RSA_PSS_SHA256, - SignatureScheme::RSA_PSS_SHA384 => webpki::alg_id::RSA_PSS_SHA384, - SignatureScheme::RSA_PSS_SHA512 => webpki::alg_id::RSA_PSS_SHA512, + SignatureScheme::RSA_PSS_SHA256 => alg_id::RSA_PSS_SHA256, + SignatureScheme::RSA_PSS_SHA384 => alg_id::RSA_PSS_SHA384, + SignatureScheme::RSA_PSS_SHA512 => alg_id::RSA_PSS_SHA512, _ => unimplemented!(), } @@ -95,7 +95,7 @@ fn rsa_verifier_from_params( key: &boring::pkey::PKeyRef, digest: MessageDigest, padding: Padding, -) -> boring::sign::Verifier { +) -> boring::sign::Verifier<'_> { let mut verifier = boring::sign::Verifier::new(digest, key).expect("failed getting verifier"); verifier .set_rsa_padding(padding) diff --git a/boring-sys-additions/src/lib.rs b/boring-sys-additions/src/lib.rs index 3243570..5a2523e 100644 --- a/boring-sys-additions/src/lib.rs +++ b/boring-sys-additions/src/lib.rs @@ -1,7 +1,7 @@ use std::ffi; extern "C" { - /// Calculates `out_len` bytes of the TLS PDF, using `digest`, and + /// Calculates `out_len` bytes of the TLS PRF, using `digest`, and /// writes them to `out`. It returns one on success and zero on error. /// /// This isn't part of the public headers in `BoringSSL` but it is exported diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 9d2084d..11b0573 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -7,15 +7,7 @@ description = "Boring Rustls provider example code and tests." publish = false [dependencies] -docopt = "~1.1" -env_logger = "0.10" -log = { version = "0.4.4" } -mio = { version = "0.8", features = ["net", "os-poll"] } -pki-types = { package = "rustls-pki-types", version = "0.2" } -rcgen = { version = "0.11.3", features = ["pem"], default-features = false } +env_logger = "0.11" rustls = { workspace = true, features = [ "logging", "std" ]} boring-rustls-provider = { path = "../boring-rustls-provider", features = ["logging"] } -rustls-pemfile = { workspace = true } -serde = "1.0" -serde_derive = "1.0" webpki-roots = { workspace = true } From 271acbb3154b2ab87187da3a4e636d0fcc19b4d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20R=C3=BCth?= <1324490+janrueth@users.noreply.github.com> Date: Fri, 10 Apr 2026 12:09:47 +0200 Subject: [PATCH 05/10] Bump boring to v5, align FIPS to SP 800-52r2, clean up features - Bump boring/boring-sys from v4 to v5 (zero API breaks) - Merge fips/fips-only into a single fips feature that both enables FIPS-validated BoringSSL and restricts algorithms to SP 800-52r2 - Tighten FIPS KX groups to P-256 and P-384 only (aligned with boring's fips202205 compliance policy) - Remove ECDSA_P521_SHA512 from FIPS signature verification set - Simplify fips feature to forward boring/fips only (drop redundant boring-sys/fips) - Add fips-precompiled as deprecated alias matching boring's naming - Change default features to empty (TLS 1.2 now requires explicit tls12 feature opt-in) - Gate TLS 1.2 code paths properly so the crate compiles and passes tests with default (TLS 1.3 only) features - Update README to reflect current state: boring v5, feature docs, FIPS mode documentation, workspace structure --- .github/workflows/ci.yml | 63 +++++++++-- Cargo.toml | 4 +- Makefile | 11 +- Readme.md | 73 +++++++++---- boring-rustls-provider/Cargo.toml | 27 ++++- boring-rustls-provider/src/aead.rs | 13 ++- boring-rustls-provider/src/aead/aes.rs | 2 + boring-rustls-provider/src/aead/chacha20.rs | 1 + boring-rustls-provider/src/lib.rs | 27 +++-- boring-rustls-provider/src/verify.rs | 16 ++- boring-rustls-provider/tests/e2e.rs | 113 ++++++++++++++++++-- 11 files changed, 273 insertions(+), 77 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 541e743..4e4eef6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,10 +9,9 @@ on: env: CARGO_TERM_COLOR: always RUSTFLAGS: -Dwarnings - FEATURES: "logging,tls12" jobs: - build: + fmt: runs-on: ubuntu-latest steps: @@ -21,9 +20,57 @@ jobs: run: sudo apt-get install -y cmake clang - name: Check fmt run: make fmt - - name: Lint - run: make lint - - name: Tests usual - run: make test - - name: Build usual - run: make build + + test-default: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Install dependencies + run: sudo apt-get install -y cmake clang + - name: Lint (default features) + run: make lint FEATURES="" + - name: Test (default features) + run: make test FEATURES="" + - name: Build (default features) + run: make build FEATURES="" + + test-tls12: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Install dependencies + run: sudo apt-get install -y cmake clang + - name: Lint (tls12) + run: make lint FEATURES="tls12" + - name: Test (tls12) + run: make test FEATURES="tls12" + - name: Build (tls12) + run: make build FEATURES="tls12" + + test-logging-tls12: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Install dependencies + run: sudo apt-get install -y cmake clang + - name: Lint (logging,tls12) + run: make lint FEATURES="logging,tls12" + - name: Test (logging,tls12) + run: make test FEATURES="logging,tls12" + - name: Build (logging,tls12) + run: make build FEATURES="logging,tls12" + + check-fips: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Install dependencies + run: sudo apt-get install -y cmake clang + - name: Check (fips) + run: cargo check -p boring-rustls-provider --all-targets --features fips + - name: Check (fips-precompiled) + run: cargo check -p boring-rustls-provider --all-targets --features fips-precompiled diff --git a/Cargo.toml b/Cargo.toml index 72a4b52..62f9a8e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,8 +17,8 @@ default-members = [ resolver = "2" [workspace.dependencies] -boring = { version = "4", default-features = false } -boring-sys = { version = "4", default-features = false } +boring = { version = "5", default-features = false } +boring-sys = { version = "5", default-features = false } rustls = { version = "0.23", default-features = false } rustls-pemfile = { version = "2" } rustls-pki-types = { version = "1" } diff --git a/Makefile b/Makefile index 5189d1e..cfa515a 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ FEATURES ?= logging,tls12 +CARGO_FEATURES := $(if $(strip $(FEATURES)),-F "$(FEATURES)",) .PHONY: fmt @@ -7,12 +8,16 @@ fmt: .PHONY: lint lint: - cargo clippy --workspace --all-targets -F "$(FEATURES)" + cargo clippy --workspace --all-targets $(CARGO_FEATURES) + +.PHONY: check +check: + cargo check --workspace --all-targets $(CARGO_FEATURES) .PHONY: test test: - cargo test --all-targets -F "$(FEATURES)" + cargo test --all-targets $(CARGO_FEATURES) .PHONY: build build: - cargo build --all-targets -F "$(FEATURES)" \ No newline at end of file + cargo build --all-targets $(CARGO_FEATURES) diff --git a/Readme.md b/Readme.md index f44e007..4a0ab65 100644 --- a/Readme.md +++ b/Readme.md @@ -2,55 +2,60 @@ [![Build Status](https://github.com/janrueth/boring-rustls-provider/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/janrueth/boring-rustls-provider/actions/workflows/ci.yml?query=branch%3Amain) -This is supposed to be the start to a [boringssl](https://github.com/cloudflare/boring)-based [rustls](https://github.com/rustls/rustls) crypto provider. +A [BoringSSL](https://github.com/cloudflare/boring)-based [rustls](https://github.com/rustls/rustls) crypto provider. -## Status -This is just a dump of me figuring out how to interface with boring and rustls. -It works to establish a connection and exchange data but I haven't written real tests yet, nor did I cleanup the code or made the effort to make it look nice. -There is probably some code in here that should rather live in the `boring` crate. +Built on `boring` v5 and `rustls` 0.23. -Further, the rustls crypto provider API is still not stable it seems. This works currently with `rustls = 0.22.0-alpha.5`. +## Features -### Supported ciphers -Currently, supports only TLS 1.3: +No features are enabled by default. The provider ships with TLS 1.3 support +out of the box; additional capabilities are opt-in. + +| Feature | Description | +|---|---| +| `fips` | Build against FIPS-validated BoringSSL and restrict the provider to FIPS-approved algorithms only (SP 800-52r2). See [FIPS mode](#fips-mode) below. | +| `fips-precompiled` | Deprecated alias for `fips`. Matches the `boring` crate's feature name. | +| `tls12` | Enable TLS 1.2 cipher suites (`ECDHE-ECDSA` and `ECDHE-RSA` with AES-GCM and ChaCha20-Poly1305). Without this only TLS 1.3 is available. | +| `logging` | Enable debug logging of BoringSSL errors and provider internals via the `log` crate. | + +## Supported Algorithms + +### Cipher Suites + +TLS 1.3 (always available): ``` AES_128_GCM_SHA256 AES_256_GCM_SHA384 CHACHA20_POLY1305_SHA256 ``` -QUIC: not yet supported - -TLS 1.2: +TLS 1.2 (requires `tls12` feature): ``` ECDHE_ECDSA_AES128_GCM_SHA256 ECDHE_RSA_AES128_GCM_SHA256 - ECDHE_ECDSA_AES256_GCM_SHA384 ECDHE_RSA_AES256_GCM_SHA384 - ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 ``` -### Key Exchange Algorithms - -`ECDHE` with curves: +### Key Exchange Groups + +ECDHE: ``` X25519 X448 -secp256r1 -secp384r1 -secp521r1 +secp256r1 (P-256) +secp384r1 (P-384) +secp521r1 (P-521) ``` - -`FFDHE` with: +FFDHE: ``` ffdhe2048 ``` -### Signature Generation / Verification +### Signature Algorithms ``` RSA_PKCS1_SHA256 @@ -66,6 +71,30 @@ ED25519 ED448 ``` +## FIPS Mode + +When the `fips` feature is enabled the provider builds against a FIPS-validated +version of BoringSSL and restricts all algorithm selections to those approved +under [SP 800-52r2](https://doi.org/10.6028/NIST.SP.800-52r2), aligned with +boring's `fips202205` compliance policy: + +- **Cipher suites**: AES-GCM only (no ChaCha20-Poly1305). +- **Key exchange groups**: P-256 and P-384 only (no X25519, X448, P-521, or FFDHE). +- **Signature algorithms**: RSA PKCS#1 / PSS and ECDSA with P-256 or P-384 only + (no P-521, Ed25519, or Ed448). + +Post-quantum hybrid key exchange (`P256Kyber768Draft00`) is planned for the +FIPS group set but not yet implemented. + +## Workspace Structure + +| Crate | Purpose | +|---|---| +| `boring-rustls-provider` | The main rustls crypto provider. | +| `boring-additions` | Safe Rust wrappers for BoringSSL APIs not yet exposed by the `boring` crate (AEAD, EVP_PKEY_CTX, HMAC_CTX). Intended for upstreaming. | +| `boring-sys-additions` | Raw FFI binding for `CRYPTO_tls1_prf` (internal BoringSSL symbol used for FIPS-compliant TLS 1.2 PRF). Intended for upstreaming. | +| `examples` | Example client binary. | ## License + MIT diff --git a/boring-rustls-provider/Cargo.toml b/boring-rustls-provider/Cargo.toml index 8a02f51..ffb9248 100644 --- a/boring-rustls-provider/Cargo.toml +++ b/boring-rustls-provider/Cargo.toml @@ -8,13 +8,30 @@ description = "Boringssl rustls provider" publish = false [features] -default = ["tls12"] -# Use a FIPS-validated version of boringssl. -fips = ["boring/fips", "boring-sys/fips"] -logging = ["log"] -fips-only = ["boring/fips", "boring-sys/fips"] +default = [] + +# Build against a FIPS-validated version of BoringSSL and restrict the +# provider to FIPS-approved algorithms only. This affects: +# - Cipher suites: AES-GCM only (no ChaCha20-Poly1305). +# - Key exchange groups: P-256 and P-384 only (no X25519, X448, P-521, +# or FFDHE). P256Kyber768Draft00 will be added once implemented. +# - Signature algorithms: RSA PKCS#1 / PSS and ECDSA with P-256/P-384 +# only (no P-521, Ed25519, or Ed448). +# Aligned with boring's `fips202205` compliance policy (SP 800-52r2). +fips = ["boring/fips"] + +# Deprecated alias for `fips`. Matches the boring crate's feature name +# for backwards compatibility. +fips-precompiled = ["fips"] + +# Enable TLS 1.2 cipher suites (ECDHE-ECDSA and ECDHE-RSA with AES-GCM +# and ChaCha20-Poly1305). Without this feature only TLS 1.3 is available. tls12 = ["rustls/tls12"] +# Enable debug logging of BoringSSL errors and provider internals via +# the `log` crate. Useful for diagnosing handshake failures. +logging = ["log"] + [dependencies] aead = {version = "0.5", default-features = false, features = ["alloc"] } boring = { workspace = true } diff --git a/boring-rustls-provider/src/aead.rs b/boring-rustls-provider/src/aead.rs index b3cc1ba..cade8a3 100644 --- a/boring-rustls-provider/src/aead.rs +++ b/boring-rustls-provider/src/aead.rs @@ -3,9 +3,9 @@ use std::marker::PhantomData; use aead::{AeadCore, AeadInPlace, Buffer, Nonce, Tag}; use boring::error::ErrorStack; use boring_additions::aead::Algorithm; -use rustls::crypto::cipher::{ - self, make_tls12_aad, make_tls13_aad, BorrowedPayload, Iv, PrefixedPayload, -}; +#[cfg(feature = "tls12")] +use rustls::crypto::cipher::make_tls12_aad; +use rustls::crypto::cipher::{self, make_tls13_aad, BorrowedPayload, Iv, PrefixedPayload}; use rustls::{ConnectionTrafficSecrets, ContentType, ProtocolVersion}; use crate::helper::log_and_map; @@ -20,6 +20,8 @@ pub(crate) trait BoringCipher { /// The IV's fixed length (Not the full IV length, only the part that doesn't change). /// Together with [`BoringCipher::explicit_nonce_len`] it determines the total /// lengths of the used nonce. + /// Used only by TLS 1.2 code paths. + #[cfg(feature = "tls12")] const FIXED_IV_LEN: usize; /// The key size in bytes const KEY_SIZE: usize; @@ -569,7 +571,8 @@ impl Buffer for EncryptBufferAdapter<'_> { #[cfg(test)] mod tests { use hex_literal::hex; - use rustls::crypto::cipher::{AeadKey, Iv}; + use rustls::crypto::cipher::AeadKey; + use rustls::crypto::cipher::Iv; use crate::aead::BoringAeadCrypter; use rustls::quic::PacketKey; @@ -611,7 +614,7 @@ mod tests { let unprotected_header = hex!("4200bff4"); let protector = BoringAeadCrypter::::new( - Iv::new(iv), + Iv::from(iv), &key, rustls::ProtocolVersion::TLSv1_3, ) diff --git a/boring-rustls-provider/src/aead/aes.rs b/boring-rustls-provider/src/aead/aes.rs index 6a5e2af..03e0f3c 100644 --- a/boring-rustls-provider/src/aead/aes.rs +++ b/boring-rustls-provider/src/aead/aes.rs @@ -11,6 +11,7 @@ impl BoringAead for Aes128 {} impl BoringCipher for Aes128 { const EXPLICIT_NONCE_LEN: usize = 8; + #[cfg(feature = "tls12")] const FIXED_IV_LEN: usize = 4; const KEY_SIZE: usize = 16; @@ -55,6 +56,7 @@ impl BoringAead for Aes256 {} impl BoringCipher for Aes256 { const EXPLICIT_NONCE_LEN: usize = 8; + #[cfg(feature = "tls12")] const FIXED_IV_LEN: usize = 4; const KEY_SIZE: usize = 32; diff --git a/boring-rustls-provider/src/aead/chacha20.rs b/boring-rustls-provider/src/aead/chacha20.rs index 2590aab..c0af24c 100644 --- a/boring-rustls-provider/src/aead/chacha20.rs +++ b/boring-rustls-provider/src/aead/chacha20.rs @@ -14,6 +14,7 @@ impl BoringAead for ChaCha20Poly1305 {} impl BoringCipher for ChaCha20Poly1305 { const EXPLICIT_NONCE_LEN: usize = 0; + #[cfg(feature = "tls12")] const FIXED_IV_LEN: usize = 12; const KEY_SIZE: usize = 32; diff --git a/boring-rustls-provider/src/lib.rs b/boring-rustls-provider/src/lib.rs index 9f39914..b4579ec 100644 --- a/boring-rustls-provider/src/lib.rs +++ b/boring-rustls-provider/src/lib.rs @@ -22,11 +22,11 @@ pub mod tls13; pub mod verify; pub fn provider() -> CryptoProvider { - #[cfg(feature = "fips-only")] + #[cfg(feature = "fips")] { provider_with_ciphers(ALL_FIPS_CIPHER_SUITES.to_vec()) } - #[cfg(not(feature = "fips-only"))] + #[cfg(not(feature = "fips"))] { provider_with_ciphers(ALL_CIPHER_SUITES.to_vec()) } @@ -35,13 +35,13 @@ pub fn provider() -> CryptoProvider { pub fn provider_with_ciphers(ciphers: Vec) -> CryptoProvider { CryptoProvider { cipher_suites: ciphers, - #[cfg(feature = "fips-only")] + #[cfg(feature = "fips")] kx_groups: ALL_FIPS_KX_GROUPS.to_vec(), - #[cfg(not(feature = "fips-only"))] + #[cfg(not(feature = "fips"))] kx_groups: ALL_KX_GROUPS.to_vec(), - #[cfg(feature = "fips-only")] + #[cfg(feature = "fips")] signature_verification_algorithms: verify::ALL_FIPS_ALGORITHMS, - #[cfg(not(feature = "fips-only"))] + #[cfg(not(feature = "fips"))] signature_verification_algorithms: verify::ALL_ALGORITHMS, secure_random: &Provider, key_provider: &Provider, @@ -99,18 +99,15 @@ static ALL_CIPHER_SUITES: &[SupportedCipherSuite] = &[ SupportedCipherSuite::Tls12(&tls12::ECDHE_RSA_AES128_GCM_SHA256), ]; -/// Allowed KX curves for FIPS are recommended -/// in [NIST SP 800-186](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-186.pdf) +/// 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. /// -/// See Sec. 3.1.2 Table 2 -/// Ordered in decending order of security strength +/// See Section 3.3.1 and 3.4.2.2. +// TODO: Add P256Kyber768Draft00 once the PQ hybrid KEM is implemented (Step 3). #[allow(unused)] pub const ALL_FIPS_KX_GROUPS: &[&dyn SupportedKxGroup] = &[ - &kx::Secp521r1 as _, // P-521 in FIPS lingo - &kx::X448 as _, // Curve448 in FIPS lingo - &kx::Secp384r1 as _, // P-384 in FIPS lingo - &kx::X25519 as _, // Curve25519 in FIPS lingo - &kx::Secp256r1 as _, // P-256 in FIPS lingo + &kx::Secp256r1 as _, // P-256 + &kx::Secp384r1 as _, // P-384 ]; #[allow(unused)] diff --git a/boring-rustls-provider/src/verify.rs b/boring-rustls-provider/src/verify.rs index 15eaea0..aa2cda2 100644 --- a/boring-rustls-provider/src/verify.rs +++ b/boring-rustls-provider/src/verify.rs @@ -61,6 +61,13 @@ pub static ALL_ALGORITHMS: WebPkiSupportedAlgorithms = WebPkiSupportedAlgorithms ], }; +/// FIPS-approved signature verification algorithms per SP 800-52r2. +/// +/// Aligned with boring's `fips202205` compliance policy: +/// - RSA: PKCS#1 v1.5 and PSS with SHA-256/384/512 +/// - ECDSA: P-256 with SHA-256 and P-384 with SHA-384 only +/// (SP 800-52r2 Table 4.1: "The curve should be P-256 or P-384") +/// - No P-521, Ed25519, or Ed448 #[allow(unused)] pub static ALL_FIPS_ALGORITHMS: WebPkiSupportedAlgorithms = WebPkiSupportedAlgorithms { all: &[ @@ -72,9 +79,6 @@ pub static ALL_FIPS_ALGORITHMS: WebPkiSupportedAlgorithms = WebPkiSupportedAlgor &rsa::BoringRsaVerifier::RSA_PSS_SHA512, &ec::BoringEcVerifier::ECDSA_NISTP256_SHA256, &ec::BoringEcVerifier::ECDSA_NISTP384_SHA384, - &ec::BoringEcVerifier::ECDSA_NISTP521_SHA512, - //&ed::BoringEdVerifier::ED25519, // FIPS 186-5: requires SHA512 but boring doesn't want us to set a digest, correct? - //&ed::BoringEdVerifier::ED448, // FIPS 186-5: requires SHAKE256 but boring doesn't want us to set a digest, correct? ], mapping: &[ ( @@ -109,11 +113,5 @@ pub static ALL_FIPS_ALGORITHMS: WebPkiSupportedAlgorithms = WebPkiSupportedAlgor SignatureScheme::ECDSA_NISTP384_SHA384, &[&ec::BoringEcVerifier::ECDSA_NISTP384_SHA384], ), - ( - SignatureScheme::ECDSA_NISTP521_SHA512, - &[&ec::BoringEcVerifier::ECDSA_NISTP521_SHA512], - ), - // (SignatureScheme::ED25519, &[&ed::BoringEdVerifier::ED25519]), - // (SignatureScheme::ED448, &[&ed::BoringEdVerifier::ED448]), ], }; diff --git a/boring-rustls-provider/tests/e2e.rs b/boring-rustls-provider/tests/e2e.rs index 21b017a..9411fd2 100644 --- a/boring-rustls-provider/tests/e2e.rs +++ b/boring-rustls-provider/tests/e2e.rs @@ -5,11 +5,12 @@ use tokio::{ net::TcpStream, }; -use boring_rustls_provider::{tls12, tls13}; -use rustls::{ - version::{TLS12, TLS13}, - ClientConfig, ServerConfig, SupportedCipherSuite, -}; +#[cfg(feature = "tls12")] +use boring_rustls_provider::tls12; +use boring_rustls_provider::tls13; +#[cfg(feature = "tls12")] +use rustls::version::TLS12; +use rustls::{version::TLS13, ClientConfig, ServerConfig, SupportedCipherSuite}; use rustls_pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}; use tokio::net::TcpListener; use tokio_rustls::{TlsAcceptor, TlsConnector}; @@ -41,17 +42,107 @@ async fn test_tls13_crypto() { } #[test] -#[cfg(any(feature = "fips", feature = "fips-only"))] +#[cfg(feature = "fips")] fn is_fips_enabled() { assert!(boring::fips::enabled()); } #[test] -#[cfg(not(any(feature = "fips", feature = "fips-only")))] +#[cfg(feature = "fips")] +fn fips_provider_excludes_chacha20_cipher_suites() { + use rustls::CipherSuite; + + let provider = boring_rustls_provider::provider(); + let disallowed = [ + CipherSuite::TLS13_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + ]; + + for suite in provider.cipher_suites { + let selected = suite.suite(); + assert!( + !disallowed.contains(&selected), + "FIPS provider exposed disallowed cipher suite: {selected:?}" + ); + } +} + +#[test] +#[cfg(feature = "fips")] +fn fips_provider_restricts_kx_groups() { + use rustls::NamedGroup; + + let provider = boring_rustls_provider::provider(); + let groups = provider + .kx_groups + .iter() + .map(|group| group.name()) + .collect::>(); + + assert!(groups.contains(&NamedGroup::secp256r1)); + assert!(groups.contains(&NamedGroup::secp384r1)); + for group in groups { + assert!( + matches!(group, NamedGroup::secp256r1 | NamedGroup::secp384r1), + "FIPS provider exposed disallowed KX group: {group:?}" + ); + } +} + +#[test] +#[cfg(feature = "fips")] +fn fips_provider_excludes_disallowed_signature_schemes() { + use rustls::SignatureScheme; + + let provider = boring_rustls_provider::provider(); + let schemes = provider + .signature_verification_algorithms + .mapping + .iter() + .map(|(scheme, _)| *scheme) + .collect::>(); + + assert!(schemes.contains(&SignatureScheme::RSA_PSS_SHA256)); + assert!(schemes.contains(&SignatureScheme::ECDSA_NISTP256_SHA256)); + + for disallowed in [ + SignatureScheme::ECDSA_NISTP521_SHA512, + SignatureScheme::ED25519, + SignatureScheme::ED448, + ] { + assert!( + !schemes.contains(&disallowed), + "FIPS provider exposed disallowed signature scheme: {disallowed:?}" + ); + } +} + +#[test] +#[cfg(not(feature = "fips"))] fn is_fips_disabled() { assert!(!boring::fips::enabled()); } +#[test] +#[cfg(not(feature = "fips"))] +fn non_fips_provider_keeps_non_fips_algorithms() { + use rustls::{CipherSuite, NamedGroup}; + + let provider = boring_rustls_provider::provider(); + + assert!(provider + .cipher_suites + .iter() + .any(|suite| { suite.suite() == CipherSuite::TLS13_CHACHA20_POLY1305_SHA256 })); + + assert!(provider + .kx_groups + .iter() + .any(|group| group.name() == NamedGroup::X25519)); +} + +#[cfg(feature = "tls12")] #[tokio::test] async fn test_tls12_ec_crypto() { let pki = TestPki::new(&rcgen::PKCS_ECDSA_P256_SHA256); @@ -78,6 +169,7 @@ async fn test_tls12_ec_crypto() { } } +#[cfg(feature = "tls12")] #[tokio::test] async fn test_tls12_rsa_crypto() { let pki = TestPki::new(&rcgen::PKCS_RSA_SHA256); @@ -188,9 +280,14 @@ impl TestPki { } fn server_config(self) -> Arc { + #[cfg(feature = "tls12")] + let versions: &[&'static rustls::SupportedProtocolVersion] = &[&TLS12, &TLS13]; + #[cfg(not(feature = "tls12"))] + let versions: &[&'static rustls::SupportedProtocolVersion] = &[&TLS13]; + let mut server_config = ServerConfig::builder_with_provider(Arc::new(boring_rustls_provider::provider())) - .with_protocol_versions(&[&TLS12, &TLS13]) + .with_protocol_versions(versions) .unwrap() .with_no_client_auth() .with_single_cert(vec![self.server_cert_der], self.server_key_der) From fafbf296b1d4b717eda3ddab9ad09198969ce6ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20R=C3=BCth?= <1324490+janrueth@users.noreply.github.com> Date: Fri, 10 Apr 2026 13:08:05 +0200 Subject: [PATCH 06/10] Add X25519MLKEM768 post-quantum hybrid key exchange Implement the X25519MLKEM768 hybrid key exchange group per draft-ietf-tls-ecdhe-mlkem-00, combining ML-KEM-768 (FIPS 203) with X25519 for post-quantum/classical hybrid key agreement. - New mlkem feature gating X25519MLKEM768 via boring's mlkem module - fips feature now implies mlkem so PQ is always available in FIPS mode - X25519MLKEM768 is the preferred (first) group in both FIPS and non-FIPS configurations when mlkem is enabled - Uses boring::mlkem for ML-KEM-768 and direct X25519 FFI for the classical component (no SPKI overhead) - Overrides start_and_complete() for server-side KEM encapsulation - Wire format: ML-KEM component first in all encodings (client share 1216 bytes, server share 1120 bytes, shared secret 64 bytes) - TLS 1.3 only (usable_for_version rejects TLS 1.2) - Unit tests: hybrid round-trip, invalid share rejection, version and FIPS flag checks - E2E tests: self-to-self PQ TLS handshake, FIPS group assertions - Cloudflare interop tests (ignored): verify kex=X25519MLKEM768 via /cdn-cgi/trace for TLS 1.3, verify classical fallback for TLS 1.2 - Update README with PQ section, feature docs, and FIPS KX updates --- .github/workflows/ci.yml | 38 ++ Readme.md | 36 +- boring-rustls-provider/Cargo.toml | 14 +- .../src/{kx.rs => kx/mod.rs} | 4 + boring-rustls-provider/src/kx/pq.rs | 364 ++++++++++++++++++ boring-rustls-provider/src/lib.rs | 35 +- boring-rustls-provider/tests/e2e.rs | 208 +++++++++- 7 files changed, 684 insertions(+), 15 deletions(-) rename boring-rustls-provider/src/{kx.rs => kx/mod.rs} (96%) create mode 100644 boring-rustls-provider/src/kx/pq.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e4eef6..29ccbbd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,6 +63,44 @@ jobs: - name: Build (logging,tls12) run: make build FEATURES="logging,tls12" + test-mlkem: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Install dependencies + run: sudo apt-get install -y cmake clang + - name: Lint (mlkem) + run: make lint FEATURES="mlkem" + - name: Test (mlkem) + run: make test FEATURES="mlkem" + - name: Build (mlkem) + run: make build FEATURES="mlkem" + + test-mlkem-tls12: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Install dependencies + run: sudo apt-get install -y cmake clang + - name: Lint (mlkem,tls12) + run: make lint FEATURES="mlkem,tls12" + - name: Test (mlkem,tls12) + run: make test FEATURES="mlkem,tls12" + - name: Build (mlkem,tls12) + run: make build FEATURES="mlkem,tls12" + + test-fips: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Install dependencies + run: sudo apt-get install -y cmake clang + - name: Test (fips) + run: cargo test -p boring-rustls-provider --all-targets --features fips + check-fips: runs-on: ubuntu-latest diff --git a/Readme.md b/Readme.md index 4a0ab65..6c86f82 100644 --- a/Readme.md +++ b/Readme.md @@ -13,8 +13,9 @@ out of the box; additional capabilities are opt-in. | Feature | Description | |---|---| -| `fips` | Build against FIPS-validated BoringSSL and restrict the provider to FIPS-approved algorithms only (SP 800-52r2). See [FIPS mode](#fips-mode) below. | +| `fips` | Build against FIPS-validated BoringSSL and restrict the provider to FIPS-approved algorithms only (SP 800-52r2). Implies `mlkem`. See [FIPS mode](#fips-mode) below. | | `fips-precompiled` | Deprecated alias for `fips`. Matches the `boring` crate's feature name. | +| `mlkem` | Enable the X25519MLKEM768 post-quantum hybrid key exchange group (`draft-ietf-tls-ecdhe-mlkem-00`). Uses ML-KEM-768 (FIPS 203) combined with X25519. See [Post-quantum key exchange](#post-quantum-key-exchange). | | `tls12` | Enable TLS 1.2 cipher suites (`ECDHE-ECDSA` and `ECDHE-RSA` with AES-GCM and ChaCha20-Poly1305). Without this only TLS 1.3 is available. | | `logging` | Enable debug logging of BoringSSL errors and provider internals via the `log` crate. | @@ -41,6 +42,11 @@ ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 ### Key Exchange Groups +Post-quantum hybrid (requires `mlkem` feature, TLS 1.3 only): +``` +X25519MLKEM768 (0x11ec) +``` + ECDHE: ``` X25519 @@ -55,6 +61,9 @@ FFDHE: ffdhe2048 ``` +When `mlkem` is enabled, X25519MLKEM768 is the preferred (first) group in both +FIPS and non-FIPS configurations. + ### Signature Algorithms ``` @@ -71,6 +80,25 @@ ED25519 ED448 ``` +## Post-Quantum Key Exchange + +The `mlkem` feature enables the **X25519MLKEM768** hybrid key exchange group +per `draft-ietf-tls-ecdhe-mlkem-00`. This combines classical X25519 +Diffie-Hellman with ML-KEM-768 (FIPS 203) post-quantum key encapsulation, +ensuring that connections are secure against both classical and quantum +adversaries. + +The `fips` feature implies `mlkem`, so X25519MLKEM768 is always available +in FIPS mode. + +Wire format (ML-KEM component first in all encodings): +- Client key share: `mlkem_pk(1184) || x25519_pk(32)` = 1216 bytes +- Server key share: `mlkem_ct(1088) || x25519_pk(32)` = 1120 bytes +- Shared secret: `mlkem_ss(32) || x25519_ss(32)` = 64 bytes + +Interoperability has been verified against Cloudflare's PQ endpoints +(`pq.cloudflareresearch.com`). + ## FIPS Mode When the `fips` feature is enabled the provider builds against a FIPS-validated @@ -79,13 +107,11 @@ under [SP 800-52r2](https://doi.org/10.6028/NIST.SP.800-52r2), aligned with boring's `fips202205` compliance policy: - **Cipher suites**: AES-GCM only (no ChaCha20-Poly1305). -- **Key exchange groups**: P-256 and P-384 only (no X25519, X448, P-521, or FFDHE). +- **Key exchange groups**: X25519MLKEM768 (preferred), P-256, and P-384 only + (no standalone X25519, X448, P-521, or FFDHE). - **Signature algorithms**: RSA PKCS#1 / PSS and ECDSA with P-256 or P-384 only (no P-521, Ed25519, or Ed448). -Post-quantum hybrid key exchange (`P256Kyber768Draft00`) is planned for the -FIPS group set but not yet implemented. - ## Workspace Structure | Crate | Purpose | diff --git a/boring-rustls-provider/Cargo.toml b/boring-rustls-provider/Cargo.toml index ffb9248..9956c2b 100644 --- a/boring-rustls-provider/Cargo.toml +++ b/boring-rustls-provider/Cargo.toml @@ -13,12 +13,18 @@ default = [] # Build against a FIPS-validated version of BoringSSL and restrict the # provider to FIPS-approved algorithms only. This affects: # - Cipher suites: AES-GCM only (no ChaCha20-Poly1305). -# - Key exchange groups: P-256 and P-384 only (no X25519, X448, P-521, -# or FFDHE). P256Kyber768Draft00 will be added once implemented. +# - Key exchange groups: X25519MLKEM768 (preferred), P-256, and P-384 +# only (no X25519, X448, P-521, or FFDHE). # - Signature algorithms: RSA PKCS#1 / PSS and ECDSA with P-256/P-384 # only (no P-521, Ed25519, or Ed448). +# Implies `mlkem` to ensure the post-quantum hybrid group is available. # Aligned with boring's `fips202205` compliance policy (SP 800-52r2). -fips = ["boring/fips"] +fips = ["boring/fips", "mlkem"] + +# Enable X25519MLKEM768 post-quantum hybrid key exchange group +# (draft-ietf-tls-ecdhe-mlkem-00). Uses ML-KEM-768 (FIPS 203) combined +# with X25519 for hybrid post-quantum/classical key agreement. +mlkem = ["boring/mlkem"] # Deprecated alias for `fips`. Matches the boring crate's feature name # for backwards compatibility. @@ -43,9 +49,11 @@ log = { version = "0.4.4", optional = true } rustls = { workspace = true } rustls-pki-types = { workspace = true } spki = "0.7" +zeroize = "1" [dev-dependencies] hex-literal = "1" rcgen = "0.12" tokio = { version = "1.34", features = ["macros", "rt", "net", "io-util", "io-std"] } tokio-rustls = { workspace = true } +webpki-roots = { workspace = true } diff --git a/boring-rustls-provider/src/kx.rs b/boring-rustls-provider/src/kx/mod.rs similarity index 96% rename from boring-rustls-provider/src/kx.rs rename to boring-rustls-provider/src/kx/mod.rs index 0796b69..9baf602 100644 --- a/boring-rustls-provider/src/kx.rs +++ b/boring-rustls-provider/src/kx/mod.rs @@ -4,6 +4,10 @@ use crate::helper::log_and_map; mod dh; mod ex; +#[cfg(feature = "mlkem")] +mod pq; +#[cfg(feature = "mlkem")] +pub(crate) use pq::X25519MlKem768; enum DhKeyType { EC((boring::ec::EcGroup, i32)), diff --git a/boring-rustls-provider/src/kx/pq.rs b/boring-rustls-provider/src/kx/pq.rs new file mode 100644 index 0000000..f5c91fe --- /dev/null +++ b/boring-rustls-provider/src/kx/pq.rs @@ -0,0 +1,364 @@ +//! X25519MLKEM768 post-quantum hybrid key exchange. +//! +//! Implements the X25519MLKEM768 hybrid key agreement per +//! `draft-ietf-tls-ecdhe-mlkem-00`. Composes ML-KEM-768 (FIPS 203) +//! with X25519, with the ML-KEM component first in all wire encodings. +//! +//! Wire format: +//! - Client key share: `mlkem_pk(1184) || x25519_pk(32)` = 1216 bytes +//! - Server key share: `mlkem_ct(1088) || x25519_pk(32)` = 1120 bytes +//! - Shared secret: `mlkem_ss(32) || x25519_ss(32)` = 64 bytes + +use boring::mlkem::{Algorithm, MlKemPrivateKey, MlKemPublicKey}; +use rustls::crypto::{self, CompletedKeyExchange, SharedSecret}; +use rustls::{Error, NamedGroup, ProtocolVersion}; +use zeroize::Zeroizing; + +const MLKEM768_PUBLIC_KEY_BYTES: usize = 1184; +const MLKEM768_CIPHERTEXT_BYTES: usize = 1088; +const X25519_PUBLIC_KEY_BYTES: usize = 32; +const X25519_PRIVATE_KEY_BYTES: usize = 32; +const X25519_SHARED_SECRET_BYTES: usize = 32; + +const CLIENT_SHARE_LEN: usize = MLKEM768_PUBLIC_KEY_BYTES + X25519_PUBLIC_KEY_BYTES; // 1216 +const SERVER_SHARE_LEN: usize = MLKEM768_CIPHERTEXT_BYTES + X25519_PUBLIC_KEY_BYTES; // 1120 + +/// X25519MLKEM768 post-quantum hybrid key exchange group. +#[derive(Debug)] +pub struct X25519MlKem768; + +impl crypto::SupportedKxGroup for X25519MlKem768 { + /// Client-side: generate ML-KEM-768 + X25519 keypairs. + fn start(&self) -> Result, Error> { + let (mlkem_pub, mlkem_priv) = + MlKemPrivateKey::generate(Algorithm::MlKem768).map_err(|e| { + crate::helper::log_and_map( + "X25519MlKem768::start mlkem generate", + e, + crypto::GetRandomFailed, + ) + })?; + + let mut x25519_pub = [0u8; X25519_PUBLIC_KEY_BYTES]; + let mut x25519_priv = Zeroizing::new([0u8; X25519_PRIVATE_KEY_BYTES]); + // SAFETY: X25519_keypair writes exactly 32 bytes to each output buffer. + unsafe { + boring_sys::X25519_keypair(x25519_pub.as_mut_ptr(), x25519_priv.as_mut_ptr()); + } + + // Wire format: mlkem_pk || x25519_pk + let mut pub_key = Vec::with_capacity(CLIENT_SHARE_LEN); + pub_key.extend_from_slice(mlkem_pub.as_bytes()); + pub_key.extend_from_slice(&x25519_pub); + + Ok(Box::new(ActiveX25519MlKem768 { + mlkem_priv, + x25519_priv, + x25519_pub, + pub_key, + })) + } + + /// Server-side: one-shot encapsulate + DH. + /// + /// Must be overridden for KEMs because the server's output (ciphertext) + /// depends on the client's input (encapsulation key). + fn start_and_complete(&self, client_share: &[u8]) -> Result { + if client_share.len() != CLIENT_SHARE_LEN { + return Err(Error::PeerMisbehaved( + rustls::PeerMisbehaved::InvalidKeyShare, + )); + } + + // Split client share: mlkem_pk(1184) || x25519_pk(32) + let (client_mlkem_pk_bytes, client_x25519_pk) = + client_share.split_at(MLKEM768_PUBLIC_KEY_BYTES); + + // ML-KEM encapsulate + let client_mlkem_pk = + MlKemPublicKey::from_slice(Algorithm::MlKem768, client_mlkem_pk_bytes).map_err( + |e| { + crate::helper::log_and_map( + "X25519MlKem768::start_and_complete mlkem parse", + e, + Error::PeerMisbehaved(rustls::PeerMisbehaved::InvalidKeyShare), + ) + }, + )?; + + let (mlkem_ct, mlkem_ss) = client_mlkem_pk.encapsulate().map_err(|e| { + crate::helper::log_and_map( + "X25519MlKem768::start_and_complete mlkem encap", + e, + Error::PeerMisbehaved(rustls::PeerMisbehaved::InvalidKeyShare), + ) + })?; + let mlkem_ss = Zeroizing::new(mlkem_ss); + + // X25519 key exchange + let mut x25519_server_pub = [0u8; X25519_PUBLIC_KEY_BYTES]; + let mut x25519_server_priv = Zeroizing::new([0u8; X25519_PRIVATE_KEY_BYTES]); + let mut x25519_ss = Zeroizing::new([0u8; X25519_SHARED_SECRET_BYTES]); + + // SAFETY: X25519_keypair writes exactly 32 bytes to each buffer. + // X25519 returns 1 on success, 0 on failure (e.g., low-order point). + unsafe { + boring_sys::X25519_keypair( + x25519_server_pub.as_mut_ptr(), + x25519_server_priv.as_mut_ptr(), + ); + let rc = boring_sys::X25519( + x25519_ss.as_mut_ptr(), + x25519_server_priv.as_ptr(), + client_x25519_pk.as_ptr(), + ); + if rc != 1 { + return Err(Error::PeerMisbehaved( + rustls::PeerMisbehaved::InvalidKeyShare, + )); + } + } + + // Server share: mlkem_ct(1088) || x25519_pk(32) + let mut server_share = Vec::with_capacity(SERVER_SHARE_LEN); + server_share.extend_from_slice(&mlkem_ct); + server_share.extend_from_slice(&x25519_server_pub); + + // Shared secret: mlkem_ss(32) || x25519_ss(32) + let mut secret = Vec::with_capacity(64); + secret.extend_from_slice(&mlkem_ss[..]); + secret.extend_from_slice(&x25519_ss[..]); + + Ok(CompletedKeyExchange { + group: self.name(), + pub_key: server_share, + secret: SharedSecret::from(secret), + }) + } + + fn name(&self) -> NamedGroup { + NamedGroup::X25519MLKEM768 + } + + fn fips(&self) -> bool { + cfg!(feature = "fips") + } + + fn usable_for_version(&self, version: ProtocolVersion) -> bool { + version == ProtocolVersion::TLSv1_3 + } +} + +/// Client-side active hybrid key exchange state. +/// +/// Holds the ML-KEM private key and X25519 private key generated +/// during [`X25519MlKem768::start`], waiting for the server's response. +struct ActiveX25519MlKem768 { + mlkem_priv: MlKemPrivateKey, + x25519_priv: Zeroizing<[u8; X25519_PRIVATE_KEY_BYTES]>, + x25519_pub: [u8; X25519_PUBLIC_KEY_BYTES], + pub_key: Vec, +} + +impl crypto::ActiveKeyExchange for ActiveX25519MlKem768 { + /// Client-side: decapsulate ML-KEM + derive X25519. + fn complete(self: Box, server_share: &[u8]) -> Result { + if server_share.len() != SERVER_SHARE_LEN { + return Err(Error::PeerMisbehaved( + rustls::PeerMisbehaved::InvalidKeyShare, + )); + } + + // Split server share: mlkem_ct(1088) || x25519_pk(32) + let (mlkem_ct, server_x25519_pk) = server_share.split_at(MLKEM768_CIPHERTEXT_BYTES); + + // ML-KEM decapsulate + let mlkem_ss = self.mlkem_priv.decapsulate(mlkem_ct).map_err(|e| { + crate::helper::log_and_map( + "ActiveX25519MlKem768::complete mlkem decap", + e, + Error::PeerMisbehaved(rustls::PeerMisbehaved::InvalidKeyShare), + ) + })?; + let mlkem_ss = Zeroizing::new(mlkem_ss); + + // X25519 derive + let mut x25519_ss = Zeroizing::new([0u8; X25519_SHARED_SECRET_BYTES]); + // SAFETY: X25519 reads 32 bytes from each input and writes 32 to output. + unsafe { + let rc = boring_sys::X25519( + x25519_ss.as_mut_ptr(), + self.x25519_priv.as_ptr(), + server_x25519_pk.as_ptr(), + ); + if rc != 1 { + return Err(Error::PeerMisbehaved( + rustls::PeerMisbehaved::InvalidKeyShare, + )); + } + } + + // Shared secret: mlkem_ss(32) || x25519_ss(32) + let mut secret = Vec::with_capacity(64); + secret.extend_from_slice(&mlkem_ss[..]); + secret.extend_from_slice(&x25519_ss[..]); + + Ok(SharedSecret::from(secret)) + } + + fn hybrid_component(&self) -> Option<(NamedGroup, &[u8])> { + Some((NamedGroup::X25519, &self.x25519_pub)) + } + + fn complete_hybrid_component( + self: Box, + peer_pub_key: &[u8], + ) -> Result { + if peer_pub_key.len() != X25519_PUBLIC_KEY_BYTES { + return Err(Error::PeerMisbehaved( + rustls::PeerMisbehaved::InvalidKeyShare, + )); + } + + let mut x25519_ss = Zeroizing::new([0u8; X25519_SHARED_SECRET_BYTES]); + + // SAFETY: X25519 reads 32 bytes from each input and writes 32 to output. + unsafe { + let rc = boring_sys::X25519( + x25519_ss.as_mut_ptr(), + self.x25519_priv.as_ptr(), + peer_pub_key.as_ptr(), + ); + if rc != 1 { + return Err(Error::PeerMisbehaved( + rustls::PeerMisbehaved::InvalidKeyShare, + )); + } + } + + Ok(SharedSecret::from(Vec::from(&x25519_ss[..]))) + } + + fn pub_key(&self) -> &[u8] { + &self.pub_key + } + + fn group(&self) -> NamedGroup { + NamedGroup::X25519MLKEM768 + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rustls::crypto::SupportedKxGroup; + + #[test] + fn hybrid_round_trip() { + let group = X25519MlKem768; + + // Client generates keypair + let client = group.start().unwrap(); + assert_eq!(client.pub_key().len(), CLIENT_SHARE_LEN); + assert_eq!(client.group(), NamedGroup::X25519MLKEM768); + + // Server encapsulates + derives + let server = group.start_and_complete(client.pub_key()).unwrap(); + assert_eq!(server.pub_key.len(), SERVER_SHARE_LEN); + assert_eq!(server.group, NamedGroup::X25519MLKEM768); + + // Client decapsulates + derives + let client_secret = client.complete(&server.pub_key).unwrap(); + + // Shared secrets must match + assert_eq!( + client_secret.secret_bytes(), + server.secret.secret_bytes(), + "client and server shared secrets differ" + ); + assert_eq!(client_secret.secret_bytes().len(), 64); + } + + #[test] + fn rejects_invalid_client_share() { + let group = X25519MlKem768; + let result = group.start_and_complete(&[0u8; 100]); + assert!(result.is_err()); + } + + #[test] + fn rejects_invalid_server_share() { + let group = X25519MlKem768; + let client = group.start().unwrap(); + let result = client.complete(&[0u8; 100]); + assert!(result.is_err()); + } + + #[test] + fn exposes_x25519_hybrid_component() { + let group = X25519MlKem768; + let client = group.start().unwrap(); + + let (component_group, component_pub_key) = client + .hybrid_component() + .expect("hybrid component should be available"); + + assert_eq!(component_group, NamedGroup::X25519); + assert_eq!(component_pub_key.len(), X25519_PUBLIC_KEY_BYTES); + assert_eq!( + component_pub_key, + &client.pub_key()[MLKEM768_PUBLIC_KEY_BYTES..CLIENT_SHARE_LEN] + ); + } + + #[test] + fn complete_hybrid_component_matches_x25519() { + let group = X25519MlKem768; + let client = group.start().unwrap(); + let (_, client_x25519_pub) = client + .hybrid_component() + .expect("hybrid component should be available"); + + let mut server_x25519_pub = [0u8; X25519_PUBLIC_KEY_BYTES]; + let mut server_x25519_priv = [0u8; X25519_PRIVATE_KEY_BYTES]; + let mut server_x25519_ss = [0u8; X25519_SHARED_SECRET_BYTES]; + + // SAFETY: X25519_keypair writes 32-byte buffers. X25519 returns 1 on success. + unsafe { + boring_sys::X25519_keypair( + server_x25519_pub.as_mut_ptr(), + server_x25519_priv.as_mut_ptr(), + ); + let rc = boring_sys::X25519( + server_x25519_ss.as_mut_ptr(), + server_x25519_priv.as_ptr(), + client_x25519_pub.as_ptr(), + ); + assert_eq!(rc, 1); + } + + let client_secret = client + .complete_hybrid_component(&server_x25519_pub) + .unwrap(); + assert_eq!(client_secret.secret_bytes(), &server_x25519_ss); + } + + #[test] + fn usable_only_for_tls13() { + let group = X25519MlKem768; + assert!(group.usable_for_version(ProtocolVersion::TLSv1_3)); + assert!(!group.usable_for_version(ProtocolVersion::TLSv1_2)); + } + + #[test] + #[cfg(feature = "fips")] + fn reports_fips() { + assert!(X25519MlKem768.fips()); + } + + #[test] + #[cfg(not(feature = "fips"))] + fn reports_non_fips() { + assert!(!X25519MlKem768.fips()); + } +} diff --git a/boring-rustls-provider/src/lib.rs b/boring-rustls-provider/src/lib.rs index b4579ec..fe822f6 100644 --- a/boring-rustls-provider/src/lib.rs +++ b/boring-rustls-provider/src/lib.rs @@ -102,16 +102,31 @@ static ALL_CIPHER_SUITES: &[SupportedCipherSuite] = &[ /// 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. /// -/// See Section 3.3.1 and 3.4.2.2. -// TODO: Add P256Kyber768Draft00 once the PQ hybrid KEM is implemented (Step 3). +/// X25519MLKEM768 is preferred when the `mlkem` feature is enabled. +/// The `fips` feature implies `mlkem`, so the PQ hybrid is always +/// available in FIPS mode. +#[cfg(feature = "mlkem")] #[allow(unused)] -pub const ALL_FIPS_KX_GROUPS: &[&dyn SupportedKxGroup] = &[ +static ALL_FIPS_KX_GROUPS: &[&dyn SupportedKxGroup] = &[ + &kx::X25519MlKem768 as _, // PQ hybrid preferred + &kx::Secp256r1 as _, // P-256 + &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. +/// Matches boring's default group preference order. +#[cfg(feature = "mlkem")] #[allow(unused)] -pub const ALL_KX_GROUPS: &[&dyn SupportedKxGroup] = &[ +static ALL_KX_GROUPS: &[&dyn SupportedKxGroup] = &[ + &kx::X25519MlKem768 as _, // PQ hybrid preferred &kx::X25519 as _, &kx::X448 as _, &kx::Secp256r1 as _, @@ -119,3 +134,15 @@ pub const ALL_KX_GROUPS: &[&dyn SupportedKxGroup] = &[ &kx::Secp521r1 as _, &kx::FfDHe2048 as _, ]; + +/// See [`ALL_KX_GROUPS`] (mlkem variant). +#[cfg(not(feature = "mlkem"))] +#[allow(unused)] +static ALL_KX_GROUPS: &[&dyn SupportedKxGroup] = &[ + &kx::X25519 as _, + &kx::Secp256r1 as _, + &kx::Secp384r1 as _, + &kx::Secp521r1 as _, + &kx::X448 as _, + &kx::FfDHe2048 as _, +]; diff --git a/boring-rustls-provider/tests/e2e.rs b/boring-rustls-provider/tests/e2e.rs index 9411fd2..68aadb1 100644 --- a/boring-rustls-provider/tests/e2e.rs +++ b/boring-rustls-provider/tests/e2e.rs @@ -41,6 +41,159 @@ async fn test_tls13_crypto() { } } +/// Self-to-self TLS 1.3 handshake using only the X25519MLKEM768 PQ hybrid group. +#[cfg(feature = "mlkem")] +#[tokio::test] +async fn test_tls13_pq_x25519_mlkem768() { + use rustls::NamedGroup; + + let pki = TestPki::new(&rcgen::PKCS_ECDSA_P256_SHA256); + + let root_store = pki.client_root_store(); + + // Build server with only X25519MLKEM768 + let server_provider = + boring_rustls_provider::provider_with_ciphers(vec![SupportedCipherSuite::Tls13( + &tls13::AES_256_GCM_SHA384, + )]); + let server_config = { + let mut cfg = ServerConfig::builder_with_provider(Arc::new(server_provider)) + .with_protocol_versions(&[&TLS13]) + .unwrap() + .with_no_client_auth() + .with_single_cert( + vec![pki.server_cert_der.clone()], + pki.server_key_der.clone_key(), + ) + .unwrap(); + cfg.key_log = Arc::new(rustls::KeyLogFile::new()); + Arc::new(cfg) + }; + + // Build client with only X25519MLKEM768 + let client_provider = + boring_rustls_provider::provider_with_ciphers(vec![SupportedCipherSuite::Tls13( + &tls13::AES_256_GCM_SHA384, + )]); + let config = ClientConfig::builder_with_provider(Arc::new(client_provider)) + .with_protocol_versions(&[&TLS13]) + .unwrap() + .with_root_certificates(root_store) + .with_no_client_auth(); + + let negotiated_group = do_exchange(config, server_config).await; + assert_eq!(negotiated_group, Some(NamedGroup::X25519MLKEM768)); +} + +/// Connect to Cloudflare's PQ test endpoint and verify X25519MLKEM768 +/// was actually negotiated by checking the `/cdn-cgi/trace` response. +/// Marked `#[ignore]` because it depends on an external service. +#[cfg(feature = "mlkem")] +#[ignore] +#[tokio::test] +async fn test_pq_interop_cloudflare() { + let mut root_store = rustls::RootCertStore::empty(); + root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); + + let provider = boring_rustls_provider::provider(); + let config = ClientConfig::builder_with_provider(Arc::new(provider)) + .with_protocol_versions(&[&TLS13]) + .unwrap() + .with_root_certificates(root_store) + .with_no_client_auth(); + + let connector = TlsConnector::from(Arc::new(config)); + let stream = TcpStream::connect("pq.cloudflareresearch.com:443") + .await + .unwrap(); + + let mut stream = connector + .connect( + rustls_pki_types::ServerName::try_from("pq.cloudflareresearch.com").unwrap(), + stream, + ) + .await + .expect("TLS handshake with pq.cloudflareresearch.com failed"); + + // Hit the trace endpoint which reports negotiated TLS parameters + stream + .write_all( + b"GET /cdn-cgi/trace HTTP/1.1\r\nHost: pq.cloudflareresearch.com\r\nConnection: close\r\n\r\n", + ) + .await + .unwrap(); + + let mut buf = Vec::new(); + stream.read_to_end(&mut buf).await.unwrap(); + let response = String::from_utf8_lossy(&buf); + + // Verify TLS 1.3 was used + assert!( + response.contains("tls=TLSv1.3"), + "expected TLSv1.3, got: {response}" + ); + + // Verify X25519MLKEM768 was negotiated as the key exchange + assert!( + response.contains("kex=X25519MLKEM768"), + "expected kex=X25519MLKEM768, got: {response}" + ); +} + +/// Connect to Cloudflare with TLS 1.2 forced and verify that a classical +/// key exchange is used (PQ groups are TLS 1.3 only). +/// Marked `#[ignore]` because it depends on an external service. +#[cfg(all(feature = "mlkem", feature = "tls12"))] +#[ignore] +#[tokio::test] +async fn test_tls12_interop_cloudflare_no_pq() { + let mut root_store = rustls::RootCertStore::empty(); + root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); + + let provider = boring_rustls_provider::provider(); + let config = ClientConfig::builder_with_provider(Arc::new(provider)) + .with_protocol_versions(&[&TLS12]) + .unwrap() + .with_root_certificates(root_store) + .with_no_client_auth(); + + let connector = TlsConnector::from(Arc::new(config)); + let stream = TcpStream::connect("pq.cloudflareresearch.com:443") + .await + .unwrap(); + + let mut stream = connector + .connect( + rustls_pki_types::ServerName::try_from("pq.cloudflareresearch.com").unwrap(), + stream, + ) + .await + .expect("TLS handshake with pq.cloudflareresearch.com (TLS 1.2) failed"); + + stream + .write_all( + b"GET /cdn-cgi/trace HTTP/1.1\r\nHost: pq.cloudflareresearch.com\r\nConnection: close\r\n\r\n", + ) + .await + .unwrap(); + + let mut buf = Vec::new(); + stream.read_to_end(&mut buf).await.unwrap(); + let response = String::from_utf8_lossy(&buf); + + // Verify TLS 1.2 was used + assert!( + response.contains("tls=TLSv1.2"), + "expected TLSv1.2, got: {response}" + ); + + // Verify a classical key exchange was used (not PQ) + assert!( + !response.contains("kex=X25519MLKEM768"), + "TLS 1.2 should not use PQ key exchange, got: {response}" + ); +} + #[test] #[cfg(feature = "fips")] fn is_fips_enabled() { @@ -80,11 +233,20 @@ fn fips_provider_restricts_kx_groups() { .map(|group| group.name()) .collect::>(); + // fips implies mlkem, so X25519MLKEM768 must be present and preferred + assert_eq!( + groups[0], + NamedGroup::X25519MLKEM768, + "X25519MLKEM768 should be the first (preferred) FIPS KX group" + ); assert!(groups.contains(&NamedGroup::secp256r1)); assert!(groups.contains(&NamedGroup::secp384r1)); - for group in groups { + for group in &groups { assert!( - matches!(group, NamedGroup::secp256r1 | NamedGroup::secp384r1), + matches!( + group, + NamedGroup::X25519MLKEM768 | NamedGroup::secp256r1 | NamedGroup::secp384r1 + ), "FIPS provider exposed disallowed KX group: {group:?}" ); } @@ -142,6 +304,35 @@ fn non_fips_provider_keeps_non_fips_algorithms() { .any(|group| group.name() == NamedGroup::X25519)); } +#[test] +#[cfg(all(not(feature = "fips"), feature = "mlkem"))] +fn non_fips_provider_includes_pq_group() { + use rustls::NamedGroup; + + let provider = boring_rustls_provider::provider(); + let groups = provider + .kx_groups + .iter() + .map(|group| group.name()) + .collect::>(); + + assert_eq!( + groups[0], + NamedGroup::X25519MLKEM768, + "X25519MLKEM768 should be the first (preferred) KX group" + ); + assert_eq!( + groups[1], + NamedGroup::X25519, + "X25519 should remain the first classical fallback" + ); + assert_eq!( + groups[2], + NamedGroup::X448, + "X448 should remain ahead of NIST P-curves in non-FIPS mode" + ); +} + #[cfg(feature = "tls12")] #[tokio::test] async fn test_tls12_ec_crypto() { @@ -200,7 +391,10 @@ async fn new_listener() -> TcpListener { TcpListener::bind("localhost:0").await.unwrap() } -async fn do_exchange(config: ClientConfig, server_config: Arc) { +async fn do_exchange( + config: ClientConfig, + server_config: Arc, +) -> Option { let listener = new_listener().await; let addr = listener.local_addr().unwrap(); tokio::spawn(spawn_echo_server(listener, server_config.clone())); @@ -216,10 +410,18 @@ async fn do_exchange(config: ClientConfig, server_config: Arc) { .await .unwrap(); + let negotiated_group = stream + .get_ref() + .1 + .negotiated_key_exchange_group() + .map(|group| group.name()); + stream.write_all(b"HELLO").await.unwrap(); let mut buf = Vec::new(); let bytes = stream.read_to_end(&mut buf).await.unwrap(); assert_eq!(&buf[..bytes], b"HELLO"); + + negotiated_group } async fn spawn_echo_server(listener: TcpListener, config: Arc) { From c71d5bbfd83f7ae2471419f252b29787103d9de2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20R=C3=BCth?= <1324490+janrueth@users.noreply.github.com> Date: Fri, 10 Apr 2026 14:58:19 +0200 Subject: [PATCH 07/10] Drop X448, P-521, FFDHE2048 to match boring's default group list --- Readme.md | 16 +--- boring-rustls-provider/src/kx/dh.rs | 123 --------------------------- boring-rustls-provider/src/kx/ex.rs | 34 +++----- boring-rustls-provider/src/kx/mod.rs | 55 ++---------- boring-rustls-provider/src/lib.rs | 43 +++------- boring-rustls-provider/tests/e2e.rs | 30 +++++-- 6 files changed, 57 insertions(+), 244 deletions(-) delete mode 100644 boring-rustls-provider/src/kx/dh.rs diff --git a/Readme.md b/Readme.md index 6c86f82..578f0b6 100644 --- a/Readme.md +++ b/Readme.md @@ -42,23 +42,13 @@ ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 ### Key Exchange Groups -Post-quantum hybrid (requires `mlkem` feature, TLS 1.3 only): -``` -X25519MLKEM768 (0x11ec) -``` +Matches boring's default supported group list: -ECDHE: ``` +X25519MLKEM768 (0x11ec, requires mlkem feature, TLS 1.3 only) X25519 -X448 secp256r1 (P-256) secp384r1 (P-384) -secp521r1 (P-521) -``` - -FFDHE: -``` -ffdhe2048 ``` 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). - **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 (no P-521, Ed25519, or Ed448). diff --git a/boring-rustls-provider/src/kx/dh.rs b/boring-rustls-provider/src/kx/dh.rs deleted file mode 100644 index 8e7839d..0000000 --- a/boring-rustls-provider/src/kx/dh.rs +++ /dev/null @@ -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, - pub_bytes: Vec, - key_type: DhKeyType, -} - -impl KeyExchange { - // Generate a new KeyExchange with a random FFDHE_2048 private key - pub fn generate_ffdhe_2048() -> Result { - 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, 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, - peer_pub_key: &[u8], - ) -> Result { - 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) - } -} diff --git a/boring-rustls-provider/src/kx/ex.rs b/boring-rustls-provider/src/kx/ex.rs index cd628a5..10fe271 100644 --- a/boring-rustls-provider/src/kx/ex.rs +++ b/boring-rustls-provider/src/kx/ex.rs @@ -1,7 +1,5 @@ -use std::{ - mem::MaybeUninit, - ptr::{self}, -}; +#[cfg(not(feature = "fips"))] +use std::{mem::MaybeUninit, ptr}; use boring::{ ec::{EcGroup, EcKey}, @@ -9,12 +7,16 @@ use boring::{ nid::Nid, pkey::{PKey, PKeyRef, Private}, }; +#[cfg(not(feature = "fips"))] use boring_additions::evp::EvpPkeyCtx; +#[cfg(not(feature = "fips"))] use foreign_types::ForeignType; use rustls::crypto; 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; @@ -30,16 +32,11 @@ pub struct KeyExchange { impl KeyExchange { /// Creates a new `KeyExchange` using a random /// private key for the `X25519` Edwards curve + #[cfg(not(feature = "fips"))] pub fn with_x25519() -> Result { 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::ed_from_curve(Nid::from_raw(boring_sys::NID_X448)) - } - /// Creates a new `KeyExchange` using a random /// private key for `sepc256r1` curve /// Also known as `X9_62_PRIME256V1` @@ -53,12 +50,6 @@ impl KeyExchange { Self::ec_from_curve(Nid::SECP384R1) } - /// Creates a new `KeyExchange` using a random - /// private key for `sep521r1` curve - pub fn with_secp521r1() -> Result { - Self::ec_from_curve(Nid::SECP521R1) - } - /// Allows getting a new `KeyExchange` using Eliptic Curves /// on the specified curve fn ec_from_curve(nid: Nid) -> Result { @@ -76,6 +67,7 @@ impl KeyExchange { /// Allows getting a new `KeyExchange` using Edwards Curves /// on the specified curve + #[cfg(not(feature = "fips"))] fn ed_from_curve(nid: Nid) -> Result { let pkey_ctx = unsafe { 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())? } + #[cfg(not(feature = "fips"))] 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)?; @@ -163,12 +155,11 @@ impl crypto::ActiveKeyExchange for KeyExchange { fn group(&self) -> rustls::NamedGroup { match self.key_type { + #[cfg(not(feature = "fips"))] 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_secp384r1)) => rustls::NamedGroup::secp384r1, - DhKeyType::EC((_, boring_sys::NID_secp521r1)) => rustls::NamedGroup::secp521r1, - _ => unimplemented!(), + _ => unreachable!("unsupported key type"), } } } @@ -190,6 +181,7 @@ mod tests { } #[test] + #[cfg(not(feature = "fips"))] fn test_derive_ed() { let alice = Box::new(KeyExchange::with_x25519().unwrap()); let bob = KeyExchange::with_x25519().unwrap(); diff --git a/boring-rustls-provider/src/kx/mod.rs b/boring-rustls-provider/src/kx/mod.rs index 9baf602..e38c295 100644 --- a/boring-rustls-provider/src/kx/mod.rs +++ b/boring-rustls-provider/src/kx/mod.rs @@ -2,23 +2,26 @@ use rustls::crypto::{self, ActiveKeyExchange}; use crate::helper::log_and_map; -mod dh; mod ex; #[cfg(feature = "mlkem")] mod pq; #[cfg(feature = "mlkem")] 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 { EC((boring::ec::EcGroup, i32)), + #[cfg(not(feature = "fips"))] ED(i32), - FFDHE2048, } /// A X25519-based key exchange +#[cfg(not(feature = "fips"))] #[derive(Debug)] pub struct X25519; +#[cfg(not(feature = "fips"))] impl crypto::SupportedKxGroup for X25519 { fn start(&self) -> Result, rustls::Error> { 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, 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 #[derive(Debug)] pub struct Secp256r1; @@ -78,35 +65,3 @@ impl crypto::SupportedKxGroup for Secp384r1 { rustls::NamedGroup::secp384r1 } } - -/// A secp521r1-based key exchange -#[derive(Debug)] -pub struct Secp521r1; - -impl crypto::SupportedKxGroup for Secp521r1 { - fn start(&self) -> Result, 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, 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 - } -} diff --git a/boring-rustls-provider/src/lib.rs b/boring-rustls-provider/src/lib.rs index fe822f6..6908297 100644 --- a/boring-rustls-provider/src/lib.rs +++ b/boring-rustls-provider/src/lib.rs @@ -66,7 +66,7 @@ impl rustls::crypto::KeyProvider for Provider { } } -#[allow(unused)] +#[cfg(feature = "fips")] static ALL_FIPS_CIPHER_SUITES: &[SupportedCipherSuite] = &[ SupportedCipherSuite::Tls13(&tls13::AES_256_GCM_SHA384), 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), ]; -#[allow(unused)] +#[cfg(not(feature = "fips"))] static ALL_CIPHER_SUITES: &[SupportedCipherSuite] = &[ SupportedCipherSuite::Tls13(&tls13::CHACHA20_POLY1305_SHA256), 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), /// aligned with boring's `fips202205` compliance policy. /// -/// X25519MLKEM768 is preferred when the `mlkem` feature is enabled. -/// The `fips` feature implies `mlkem`, so the PQ hybrid is always -/// available in FIPS mode. -#[cfg(feature = "mlkem")] -#[allow(unused)] +/// The `fips` feature implies `mlkem`, so X25519MLKEM768 is always +/// available and preferred in FIPS mode. +#[cfg(feature = "fips")] static ALL_FIPS_KX_GROUPS: &[&dyn SupportedKxGroup] = &[ &kx::X25519MlKem768 as _, // PQ hybrid preferred &kx::Secp256r1 as _, // P-256 &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. -/// Matches boring's default group preference order. -#[cfg(feature = "mlkem")] -#[allow(unused)] +/// +/// Matches boring's default supported group list exactly: +/// X25519MLKEM768 (when mlkem enabled), X25519, P-256, P-384. +#[cfg(all(not(feature = "fips"), feature = "mlkem"))] static ALL_KX_GROUPS: &[&dyn SupportedKxGroup] = &[ &kx::X25519MlKem768 as _, // PQ hybrid preferred &kx::X25519 as _, - &kx::X448 as _, &kx::Secp256r1 as _, &kx::Secp384r1 as _, - &kx::Secp521r1 as _, - &kx::FfDHe2048 as _, ]; /// See [`ALL_KX_GROUPS`] (mlkem variant). -#[cfg(not(feature = "mlkem"))] -#[allow(unused)] -static ALL_KX_GROUPS: &[&dyn SupportedKxGroup] = &[ - &kx::X25519 as _, - &kx::Secp256r1 as _, - &kx::Secp384r1 as _, - &kx::Secp521r1 as _, - &kx::X448 as _, - &kx::FfDHe2048 as _, -]; +#[cfg(not(any(feature = "fips", feature = "mlkem")))] +static ALL_KX_GROUPS: &[&dyn SupportedKxGroup] = + &[&kx::X25519 as _, &kx::Secp256r1 as _, &kx::Secp384r1 as _]; diff --git a/boring-rustls-provider/tests/e2e.rs b/boring-rustls-provider/tests/e2e.rs index 68aadb1..fa5bd5a 100644 --- a/boring-rustls-provider/tests/e2e.rs +++ b/boring-rustls-provider/tests/e2e.rs @@ -22,7 +22,13 @@ async fn test_tls13_crypto() { let root_store = pki.client_root_store(); 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_256_GCM_SHA384), SupportedCipherSuite::Tls13(&tls13::CHACHA20_POLY1305_SHA256), @@ -324,12 +330,12 @@ fn non_fips_provider_includes_pq_group() { assert_eq!( groups[1], NamedGroup::X25519, - "X25519 should remain the first classical fallback" + "X25519 should be the first classical fallback" ); assert_eq!( groups[2], - NamedGroup::X448, - "X448 should remain ahead of NIST P-curves in non-FIPS mode" + NamedGroup::secp256r1, + "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 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_AES256_GCM_SHA384), 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 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_AES256_GCM_SHA384), SupportedCipherSuite::Tls12(&tls12::ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256), From bbd0ccf0b8757b2c6d1634903258500146d49b8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20R=C3=BCth?= <1324490+janrueth@users.noreply.github.com> Date: Fri, 10 Apr 2026 14:13:22 +0200 Subject: [PATCH 08/10] Harden key material zeroization paths Reduce secret lifetime in HKDF and HMAC internals, avoid extra shared-secret copying in key exchange, and add SHA384 HMAC coverage to guard output sizing. --- boring-rustls-provider/src/hkdf.rs | 11 +++++----- boring-rustls-provider/src/hmac.rs | 31 +++++++++++++++++++++++------ boring-rustls-provider/src/kx/ex.rs | 2 +- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/boring-rustls-provider/src/hkdf.rs b/boring-rustls-provider/src/hkdf.rs index 64c37c4..cccaaf2 100644 --- a/boring-rustls-provider/src/hkdf.rs +++ b/boring-rustls-provider/src/hkdf.rs @@ -2,6 +2,7 @@ use std::marker::PhantomData; use boring::hash::MessageDigest; use rustls::crypto::tls13::{self, Hkdf as RustlsHkdf}; +use zeroize::Zeroizing; use crate::helper::{cvt, cvt_p}; @@ -67,7 +68,7 @@ impl RustlsHkdf for Hkdf { let digest = T::new_hash(); let hash_size = digest.size(); - let mut prk = [0u8; boring_sys::EVP_MAX_MD_SIZE as usize]; + let mut prk = Zeroizing::new([0u8; boring_sys::EVP_MAX_MD_SIZE as usize]); let mut prk_len = 0; // if salt isn't set we usen these bytes here as salt @@ -103,7 +104,7 @@ impl RustlsHkdf for Hkdf { okm: &rustls::crypto::tls13::OkmBlock, ) -> Box { let okm = okm.as_ref(); - let mut prk = [0u8; boring_sys::EVP_MAX_MD_SIZE as usize]; + let mut prk = Zeroizing::new([0u8; boring_sys::EVP_MAX_MD_SIZE as usize]); let prk_len = okm.len(); prk[..prk_len].copy_from_slice(okm); @@ -121,7 +122,7 @@ impl RustlsHkdf for Hkdf { message: &[u8], ) -> rustls::crypto::hmac::Tag { let digest = T::new_hash(); - let mut hash = [0u8; boring_sys::EVP_MAX_MD_SIZE as usize]; + let mut hash = Zeroizing::new([0u8; boring_sys::EVP_MAX_MD_SIZE as usize]); let mut hash_len = 0u32; unsafe { cvt_p(boring_sys::HMAC( @@ -140,7 +141,7 @@ impl RustlsHkdf for Hkdf { } struct HkdfExpander { - prk: [u8; boring_sys::EVP_MAX_MD_SIZE as usize], + prk: Zeroizing<[u8; boring_sys::EVP_MAX_MD_SIZE as usize]>, prk_len: usize, digest: MessageDigest, } @@ -187,7 +188,7 @@ impl tls13::HkdfExpander for HkdfExpander { /// This is infallible, because by definition `OkmBlock` is always exactly /// `HashLen` bytes long. fn expand_block(&self, info: &[&[u8]]) -> tls13::OkmBlock { - let mut output = [0u8; boring_sys::EVP_MAX_MD_SIZE as usize]; + let mut output = Zeroizing::new([0u8; boring_sys::EVP_MAX_MD_SIZE as usize]); let output_len = self.hash_len(); self.expand_slice(info, &mut output[..output_len]) diff --git a/boring-rustls-provider/src/hmac.rs b/boring-rustls-provider/src/hmac.rs index c5d403c..e0d5e49 100644 --- a/boring-rustls-provider/src/hmac.rs +++ b/boring-rustls-provider/src/hmac.rs @@ -4,6 +4,7 @@ use boring::hash::MessageDigest; use boring_additions::hmac::HmacCtx; use foreign_types::ForeignType; use rustls::crypto; +use zeroize::Zeroizing; use crate::helper::{cvt, cvt_p}; @@ -30,7 +31,7 @@ impl crypto::hmac::Hmac for BoringHmac { Box::new(BoringHmacKey { ctx, md, - key: key.to_vec(), + key: Zeroizing::new(key.to_vec()), }) } @@ -41,11 +42,10 @@ impl crypto::hmac::Hmac for BoringHmac { } } -#[derive(Clone)] struct BoringHmacKey { ctx: HmacCtx, md: MessageDigest, - key: Vec, + key: Zeroizing>, } impl BoringHmacKey { @@ -99,8 +99,8 @@ impl crypto::hmac::Key for BoringHmacKey { self.update(last); - let mut out = [0u8; 32]; - let out_len = self.finish(&mut out); + let mut out = Zeroizing::new([0u8; boring_sys::EVP_MAX_MD_SIZE as usize]); + let out_len = self.finish(&mut out[..]); crypto::hmac::Tag::new(&out[..out_len]) } @@ -112,7 +112,7 @@ impl crypto::hmac::Key for BoringHmacKey { #[cfg(test)] mod tests { - use super::SHA256; + use super::{SHA256, SHA384}; use hex_literal::hex; #[test] @@ -141,4 +141,23 @@ mod tests { hex!("11fa4a6ee97bebfad9e1087145c556fec9a786cad0659aa10702d21bd2968305") ); } + + #[test] + fn test_sha384_hmac_len() { + let hasher = SHA384.with_key("Very Secret".as_bytes()); + + let tag = hasher.sign_concat( + &[], + &[ + "yay".as_bytes(), + "this".as_bytes(), + "works".as_bytes(), + "well".as_bytes(), + ], + &[], + ); + + assert_eq!(tag.as_ref().len(), hasher.tag_len()); + assert_eq!(tag.as_ref().len(), 48); + } } diff --git a/boring-rustls-provider/src/kx/ex.rs b/boring-rustls-provider/src/kx/ex.rs index 10fe271..5e80694 100644 --- a/boring-rustls-provider/src/kx/ex.rs +++ b/boring-rustls-provider/src/kx/ex.rs @@ -139,7 +139,7 @@ impl crypto::ActiveKeyExchange for KeyExchange { peer_pub_key: &[u8], ) -> Result { self.diffie_hellman(peer_pub_key) - .map(|x| crypto::SharedSecret::from(x.as_slice())) + .map(crypto::SharedSecret::from) .map_err(|e| { log_and_map( "ex::KeyExchange::diffie_hellman", From b88c87235dc43c85bb7d93cf9c9f039a865aec00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20R=C3=BCth?= <1324490+janrueth@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:30:01 +0200 Subject: [PATCH 09/10] Harden crypto provider error handling and FIPS reporting Implement provider-wide FIPS semantics by filtering non-FIPS suites in provider_with_ciphers() and wiring fips() reporting across provider components, KX groups, AEADs, and signature verifiers. Replace panic-prone hotpath behavior with error returns across TLS/QUIC AEAD setup and header protection, enforce HKDF output bounds, and remove shared HMAC context cloning to tighten runtime safety. Rework signing and verification paths to support SEC1 EC key loading, curve-aware scheme selection, and consistent malformed-input error handling without panics. Add comprehensive regression coverage for malformed KX shares, verifier inputs, AEAD truncation and constructor failures, plus a panic-surface test that scans runtime provider/additions code for new panic constructs unless explicitly allowlisted. Update the example client to return Result and eliminate non-test unwrap-style exits. --- boring-additions/src/aead/mod.rs | 55 ++- boring-additions/src/hmac/types.rs | 16 - boring-rustls-provider/src/aead.rs | 384 +++++++++++++++--- boring-rustls-provider/src/aead/aes.rs | 33 +- boring-rustls-provider/src/aead/chacha20.rs | 22 +- boring-rustls-provider/src/hash.rs | 6 +- boring-rustls-provider/src/hkdf.rs | 51 ++- boring-rustls-provider/src/hmac.rs | 48 +-- boring-rustls-provider/src/kx/ex.rs | 28 +- boring-rustls-provider/src/kx/mod.rs | 12 + boring-rustls-provider/src/lib.rs | 29 ++ boring-rustls-provider/src/prf.rs | 4 + boring-rustls-provider/src/sign.rs | 313 ++++++++++---- boring-rustls-provider/src/verify/ec.rs | 12 +- boring-rustls-provider/src/verify/ed.rs | 8 +- boring-rustls-provider/src/verify/rsa.rs | 23 +- boring-rustls-provider/tests/e2e.rs | 185 +++++++-- boring-rustls-provider/tests/panic_surface.rs | 244 +++++++++++ examples/src/bin/client.rs | 29 +- 19 files changed, 1230 insertions(+), 272 deletions(-) create mode 100644 boring-rustls-provider/tests/panic_surface.rs diff --git a/boring-additions/src/aead/mod.rs b/boring-additions/src/aead/mod.rs index 353d440..2803393 100644 --- a/boring-additions/src/aead/mod.rs +++ b/boring-additions/src/aead/mod.rs @@ -78,11 +78,7 @@ impl Crypter { /// /// # Errors /// Returns the `BoringSSL` error in case of an internal error - /// - /// # Panics - /// * If the key length mismatches the `aead_alg` required key length pub fn new(aead_alg: &Algorithm, key: &[u8]) -> Result { - assert_eq!(aead_alg.key_length(), key.len()); boring_sys::init(); let this = unsafe { @@ -116,10 +112,6 @@ impl Crypter { /// /// # Errors /// In case of an error, returns the `BoringSSL` error - /// - /// # Panics - /// * If the `nonce` is not the expected lenght - /// * If the `tag` has not enough space pub fn seal_in_place( &self, nonce: &[u8], @@ -127,8 +119,9 @@ impl Crypter { buffer: &mut [u8], tag: &mut [u8], ) -> Result { - assert!(tag.len() >= self.max_overhead); - assert_eq!(nonce.len(), self.nonce_len); + if tag.len() < self.max_overhead || nonce.len() != self.nonce_len { + return Err(ErrorStack::get()); + } let mut tag_len = tag.len(); unsafe { @@ -156,9 +149,6 @@ impl Crypter { /// /// # Errors /// In case of an error, returns the `BoringSSL` error - /// - /// # Panics - /// * if the nonce has the wrong lenght pub fn open_in_place( &self, nonce: &[u8], @@ -166,7 +156,9 @@ impl Crypter { buffer: &mut [u8], tag: &[u8], ) -> Result<(), ErrorStack> { - assert_eq!(nonce.len(), self.nonce_len); + if nonce.len() != self.nonce_len { + return Err(ErrorStack::get()); + } unsafe { cvt(boring_sys::EVP_AEAD_CTX_open_gather( @@ -188,11 +180,11 @@ impl Crypter { #[cfg(test)] mod tests { - use super::Crypter; + use super::{Algorithm, Crypter}; #[test] fn in_out() { - let key = Crypter::new(&super::Algorithm::aes_128_gcm(), &[0u8; 16]).unwrap(); + let key = Crypter::new(&Algorithm::aes_128_gcm(), &[0u8; 16]).unwrap(); let nonce = [0u8; 12]; let associated_data = b"this is authenticated"; let mut buffer = Vec::with_capacity(26); @@ -207,4 +199,35 @@ mod tests { assert_eq!(b"ABCDE", buffer.as_slice()); } + + #[test] + fn new_rejects_invalid_key_length() { + let result = Crypter::new(&Algorithm::aes_128_gcm(), &[0u8; 15]); + + assert!(result.is_err()); + } + + #[test] + fn seal_rejects_invalid_nonce_and_tag_lengths() { + let key = Crypter::new(&Algorithm::aes_128_gcm(), &[0u8; 16]).unwrap(); + let mut payload = [0u8; 8]; + + let mut short_tag = [0u8; 8]; + let short_tag_result = key.seal_in_place(&[0u8; 12], b"", &mut payload, &mut short_tag); + assert!(short_tag_result.is_err()); + + let mut tag = [0u8; 16]; + let wrong_nonce_result = key.seal_in_place(&[0u8; 11], b"", &mut payload, &mut tag); + assert!(wrong_nonce_result.is_err()); + } + + #[test] + fn open_rejects_invalid_nonce_length() { + let key = Crypter::new(&Algorithm::aes_128_gcm(), &[0u8; 16]).unwrap(); + let mut payload = [0u8; 8]; + let tag = [0u8; 16]; + + let result = key.open_in_place(&[0u8; 11], b"", &mut payload, &tag); + assert!(result.is_err()); + } } diff --git a/boring-additions/src/hmac/types.rs b/boring-additions/src/hmac/types.rs index 7601eb9..ad3661c 100644 --- a/boring-additions/src/hmac/types.rs +++ b/boring-additions/src/hmac/types.rs @@ -5,8 +5,6 @@ use std::{ use foreign_types::{ForeignType, ForeignTypeRef, Opaque}; -use crate::helper::{cvt, cvt_p}; - pub struct HmacCtxRef(Opaque); unsafe impl ForeignTypeRef for HmacCtxRef { @@ -35,20 +33,6 @@ unsafe impl ForeignType for HmacCtx { } } -impl Clone for HmacCtx { - fn clone(&self) -> Self { - unsafe { - cvt_p(boring_sys::HMAC_CTX_new()) - .map(|ctx| HmacCtx::from_ptr(ctx)) - .and_then(|ctx| { - cvt(boring_sys::HMAC_CTX_copy(ctx.as_ptr(), self.0.as_ptr()))?; - Ok(ctx) - }) - } - .expect("failed cloning hmac ctx") - } -} - impl Drop for HmacCtx { fn drop(&mut self) { unsafe { diff --git a/boring-rustls-provider/src/aead.rs b/boring-rustls-provider/src/aead.rs index cade8a3..6bcd3ee 100644 --- a/boring-rustls-provider/src/aead.rs +++ b/boring-rustls-provider/src/aead.rs @@ -35,6 +35,9 @@ pub(crate) trait BoringCipher { /// confidentiality limit const CONFIDENTIALITY_LIMIT: u64; + /// Whether this algorithm is FIPS-approved. + const FIPS_APPROVED: bool; + /// Constructs a new instance of this cipher as an AEAD algorithm fn new_cipher() -> Algorithm; @@ -49,7 +52,7 @@ pub(crate) trait QuicCipher: Send + Sync { /// the expected length of a sample const SAMPLE_LEN: usize; - fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> [u8; 5]; + fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> Result<[u8; 5], rustls::Error>; } pub(crate) trait BoringAead: BoringCipher + AeadCore + Send + Sync {} @@ -74,19 +77,21 @@ impl AeadCore for BoringAeadCrypter { impl BoringAeadCrypter { /// Creates a new aead crypter pub fn new(iv: Iv, key: &[u8], tls_version: ProtocolVersion) -> Result { - assert!(match tls_version { + let tls_version_supported = match tls_version { #[cfg(feature = "tls12")] ProtocolVersion::TLSv1_2 => true, ProtocolVersion::TLSv1_3 => true, _ => false, - }); + }; + if !tls_version_supported { + return Err(ErrorStack::get()); + } let cipher = ::new_cipher(); - assert_eq!( - cipher.nonce_len(), - rustls::crypto::cipher::Nonce::new(&iv, 0).0.len() - ); + if cipher.nonce_len() != rustls::crypto::cipher::Nonce::new(&iv, 0).0.len() { + return Err(ErrorStack::get()); + } let crypter = BoringAeadCrypter { crypter: boring_additions::aead::Crypter::new(&cipher, key)?, @@ -181,18 +186,21 @@ where ) }) } - _ => unimplemented!(), + _ => Err(rustls::Error::EncryptError), } } fn encrypted_payload_len(&self, payload_len: usize) -> usize { - match self.tls_version { - ProtocolVersion::TLSv1_2 => { - payload_len + self.crypter.max_overhead() + ::EXPLICIT_NONCE_LEN - } - ProtocolVersion::TLSv1_3 => payload_len + 1 + self.crypter.max_overhead(), - _ => unimplemented!(), - } + let per_record_overhead = match self.tls_version { + ProtocolVersion::TLSv1_2 => self + .crypter + .max_overhead() + .saturating_add(::EXPLICIT_NONCE_LEN), + ProtocolVersion::TLSv1_3 => 1usize.saturating_add(self.crypter.max_overhead()), + _ => return payload_len, + }; + + payload_len.saturating_add(per_record_overhead) } } @@ -209,10 +217,17 @@ where #[cfg(feature = "tls12")] ProtocolVersion::TLSv1_2 => { let explicit_nonce_len = ::EXPLICIT_NONCE_LEN; + let tag_len = self.crypter.max_overhead(); + let min_payload_len = explicit_nonce_len + .checked_add(tag_len) + .ok_or(rustls::Error::DecryptError)?; + + if m.payload.len() < min_payload_len { + return Err(rustls::Error::DecryptError); + } // payload is: [nonce] | [ciphertext] | [auth tag] - let actual_payload_length = - m.payload.len() - self.crypter.max_overhead() - explicit_nonce_len; + let actual_payload_length = m.payload.len() - tag_len - explicit_nonce_len; let aad = make_tls12_aad(seq, m.typ, m.version, actual_payload_length); @@ -224,7 +239,9 @@ where let nonce = { let fixed_iv_len = ::FIXED_IV_LEN; - assert_eq!(explicit_nonce_len + fixed_iv_len, 12); + if explicit_nonce_len + fixed_iv_len != 12 { + return Err(rustls::Error::DecryptError); + } // grab the IV by constructing a nonce, this is just an xor let iv = cipher::Nonce::new(&self.iv, seq).0; @@ -235,8 +252,7 @@ where }; // split off the authentication tag - let (payload, tag) = - payload.split_at_mut(payload.len() - self.crypter.max_overhead()); + let (payload, tag) = payload.split_at_mut(payload.len() - tag_len); self.crypter .open_in_place(&nonce, &aad, payload, tag) @@ -262,7 +278,7 @@ where .map_err(|_| rustls::Error::DecryptError) .and_then(|_| m.into_tls13_unpadded_message()) } - _ => unimplemented!(), + _ => Err(rustls::Error::DecryptError), } } } @@ -295,6 +311,10 @@ where let associated_data = header; let nonce = cipher::Nonce::new(&self.iv, packet_number); + if payload.len() < self.crypter.max_overhead() { + return Err(rustls::Error::DecryptError); + } + let (buffer, tag) = payload.split_at_mut(payload.len() - self.crypter.max_overhead()); self.crypter @@ -317,6 +337,68 @@ where } } +struct InvalidMessageEncrypter; + +impl cipher::MessageEncrypter for InvalidMessageEncrypter { + fn encrypt( + &mut self, + _msg: cipher::OutboundPlainMessage<'_>, + _seq: u64, + ) -> Result { + Err(rustls::Error::EncryptError) + } + + fn encrypted_payload_len(&self, payload_len: usize) -> usize { + payload_len + } +} + +struct InvalidMessageDecrypter; + +impl cipher::MessageDecrypter for InvalidMessageDecrypter { + fn decrypt<'a>( + &mut self, + _msg: cipher::InboundOpaqueMessage<'a>, + _seq: u64, + ) -> Result, rustls::Error> { + Err(rustls::Error::DecryptError) + } +} + +struct InvalidPacketKey(PhantomData); + +impl rustls::quic::PacketKey for InvalidPacketKey { + fn encrypt_in_place( + &self, + _packet_number: u64, + _header: &[u8], + _payload: &mut [u8], + ) -> Result { + Err(rustls::Error::EncryptError) + } + + fn decrypt_in_place<'a>( + &self, + _packet_number: u64, + _header: &[u8], + _payload: &'a mut [u8], + ) -> Result<&'a [u8], rustls::Error> { + Err(rustls::Error::DecryptError) + } + + fn tag_len(&self) -> usize { + ::TAG_LEN + } + + fn confidentiality_limit(&self) -> u64 { + 0 + } + + fn integrity_limit(&self) -> u64 { + 0 + } +} + pub(crate) struct Aead(PhantomData); impl Aead { @@ -325,17 +407,31 @@ impl Aead { impl cipher::Tls13AeadAlgorithm for Aead { fn encrypter(&self, key: cipher::AeadKey, iv: cipher::Iv) -> Box { - Box::new( - BoringAeadCrypter::::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3) - .expect("failed to create AEAD crypter"), - ) + match BoringAeadCrypter::::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3) { + Ok(crypter) => Box::new(crypter), + Err(err) => { + log_and_map( + "Tls13AeadAlgorithm::encrypter BoringAeadCrypter::new", + err, + (), + ); + Box::new(InvalidMessageEncrypter) + } + } } fn decrypter(&self, key: cipher::AeadKey, iv: cipher::Iv) -> Box { - Box::new( - BoringAeadCrypter::::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3) - .expect("failed to create AEAD crypter"), - ) + match BoringAeadCrypter::::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3) { + Ok(crypter) => Box::new(crypter), + Err(err) => { + log_and_map( + "Tls13AeadAlgorithm::decrypter BoringAeadCrypter::new", + err, + (), + ); + Box::new(InvalidMessageDecrypter) + } + } } fn key_len(&self) -> usize { @@ -349,6 +445,10 @@ impl cipher::Tls13AeadAlgorithm for Aead { ) -> Result { Ok(::extract_keys(key, iv)) } + + fn fips(&self) -> bool { + cfg!(feature = "fips") && ::FIPS_APPROVED + } } #[cfg(feature = "tls12")] @@ -362,24 +462,42 @@ impl cipher::Tls12AeadAlgorithm for Aead { let mut full_iv = Vec::with_capacity(iv.len() + extra.len()); full_iv.extend_from_slice(iv); full_iv.extend_from_slice(extra); - Box::new( - BoringAeadCrypter::::new(Iv::copy(&full_iv), key.as_ref(), ProtocolVersion::TLSv1_2) - .expect("failed to create AEAD crypter"), - ) + match BoringAeadCrypter::::new( + Iv::copy(&full_iv), + key.as_ref(), + ProtocolVersion::TLSv1_2, + ) { + Ok(crypter) => Box::new(crypter), + Err(err) => { + log_and_map( + "Tls12AeadAlgorithm::encrypter BoringAeadCrypter::new", + err, + (), + ); + Box::new(InvalidMessageEncrypter) + } + } } fn decrypter(&self, key: cipher::AeadKey, iv: &[u8]) -> Box { let mut pseudo_iv = Vec::with_capacity(iv.len() + ::EXPLICIT_NONCE_LEN); pseudo_iv.extend_from_slice(iv); pseudo_iv.extend_from_slice(&vec![0u8; ::EXPLICIT_NONCE_LEN]); - Box::new( - BoringAeadCrypter::::new( - Iv::copy(&pseudo_iv), - key.as_ref(), - ProtocolVersion::TLSv1_2, - ) - .expect("failed to create AEAD crypter"), - ) + match BoringAeadCrypter::::new( + Iv::copy(&pseudo_iv), + key.as_ref(), + ProtocolVersion::TLSv1_2, + ) { + Ok(crypter) => Box::new(crypter), + Err(err) => { + log_and_map( + "Tls12AeadAlgorithm::decrypter BoringAeadCrypter::new", + err, + (), + ); + Box::new(InvalidMessageDecrypter) + } + } } fn key_block_shape(&self) -> cipher::KeyBlockShape { @@ -399,7 +517,13 @@ impl cipher::Tls12AeadAlgorithm for Aead { let nonce = { let fixed_iv_len = ::FIXED_IV_LEN; let explicit_nonce_len = ::EXPLICIT_NONCE_LEN; - assert_eq!(explicit_nonce_len + fixed_iv_len, 12); + if explicit_nonce_len + fixed_iv_len != 12 { + return Err(cipher::UnsupportedOperationError); + } + + if iv.len() != fixed_iv_len || explicit.len() != explicit_nonce_len { + return Err(cipher::UnsupportedOperationError); + } // grab the IV by constructing a nonce, this is just an xor @@ -410,6 +534,10 @@ impl cipher::Tls12AeadAlgorithm for Aead { }; Ok(::extract_keys(key, Iv::copy(&nonce))) } + + fn fips(&self) -> bool { + cfg!(feature = "fips") && ::FIPS_APPROVED + } } struct QuicHeaderProtector { @@ -425,8 +553,8 @@ impl QuicHeaderProtector { first: &mut u8, packet_number: &mut [u8], remove: bool, - ) { - let mask = T::header_protection_mask(self.key.as_ref(), sample); + ) -> Result<(), rustls::Error> { + let mask = T::header_protection_mask(self.key.as_ref(), sample)?; const LONG_HEADER_FORMAT: u8 = 0x80; let bits_to_mask = if (*first & LONG_HEADER_FORMAT) == LONG_HEADER_FORMAT { @@ -453,6 +581,8 @@ impl QuicHeaderProtector { for (pn_byte, m) in packet_number.iter_mut().zip(&mask[1..]).take(pn_length) { *pn_byte ^= m; } + + Ok(()) } } @@ -468,7 +598,7 @@ impl rustls::quic::HeaderProtectionKey for QuicHeaderProtector return Err(rustls::Error::General("packet number too long".into())); } - self.rfc9001_header_protection(sample, first, packet_number, false); + self.rfc9001_header_protection(sample, first, packet_number, false)?; Ok(()) } @@ -483,7 +613,7 @@ impl rustls::quic::HeaderProtectionKey for QuicHeaderProtector return Err(rustls::Error::General("packet number too long".into())); } - self.rfc9001_header_protection(sample, first, packet_number, true); + self.rfc9001_header_protection(sample, first, packet_number, true)?; Ok(()) } @@ -498,10 +628,13 @@ where T: QuicCipher + BoringAead + 'static, { fn packet_key(&self, key: cipher::AeadKey, iv: Iv) -> Box { - Box::new( - BoringAeadCrypter::::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3) - .expect("failed to create AEAD crypter"), - ) + match BoringAeadCrypter::::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3) { + Ok(crypter) => Box::new(crypter), + Err(err) => { + log_and_map("QuicAlgorithm::packet_key BoringAeadCrypter::new", err, ()); + Box::new(InvalidPacketKey::(PhantomData)) + } + } } fn header_protection_key( @@ -517,6 +650,10 @@ where fn aead_key_len(&self) -> usize { ::KEY_SIZE } + + fn fips(&self) -> bool { + cfg!(feature = "fips") && ::FIPS_APPROVED + } } struct DecryptBufferAdapter<'a, 'p>(&'a mut BorrowedPayload<'p>); @@ -535,7 +672,7 @@ impl AsMut<[u8]> for DecryptBufferAdapter<'_, '_> { impl Buffer for DecryptBufferAdapter<'_, '_> { fn extend_from_slice(&mut self, _: &[u8]) -> aead::Result<()> { - unreachable!("not used by `AeadInPlace::decrypt_in_place`") + Err(aead::Error) } fn truncate(&mut self, len: usize) { @@ -571,13 +708,20 @@ impl Buffer for EncryptBufferAdapter<'_> { #[cfg(test)] mod tests { use hex_literal::hex; - use rustls::crypto::cipher::AeadKey; - use rustls::crypto::cipher::Iv; + use rustls::crypto::cipher::Tls13AeadAlgorithm; + use rustls::crypto::cipher::{AeadKey, InboundOpaqueMessage, Iv}; + #[cfg(feature = "tls12")] + use rustls::crypto::cipher::{MessageDecrypter, Tls12AeadAlgorithm}; + use rustls::quic::Algorithm as QuicAlgorithm; + use rustls::quic::HeaderProtectionKey; + use rustls::ContentType; + use rustls::ProtocolVersion; use crate::aead::BoringAeadCrypter; use rustls::quic::PacketKey; - use super::{chacha20::ChaCha20Poly1305, QuicHeaderProtector}; + use super::aes::Aes128; + use super::{chacha20::ChaCha20Poly1305, BoringCipher, QuicHeaderProtector}; #[test] fn quic_header_protection_short() { @@ -594,14 +738,97 @@ mod tests { phantom: std::marker::PhantomData::, }; - protector.rfc9001_header_protection(&sample, &mut first[0], packet_number, false); + protector + .rfc9001_header_protection(&sample, &mut first[0], packet_number, false) + .expect("valid sample should protect QUIC header"); assert_eq!(&header[..], &protected_header[..]); let (first, packet_number) = header.split_at_mut(1); - protector.rfc9001_header_protection(&sample, &mut first[0], packet_number, true); + protector + .rfc9001_header_protection(&sample, &mut first[0], packet_number, true) + .expect("valid sample should unprotect QUIC header"); assert_eq!(&header[..], &unprotected_header[..]); } + #[test] + fn quic_header_protection_rejects_invalid_sample_without_mutation() { + let hp_key = hex!("25a282b9e82f06f21f488917a4fc8f1b73573685608597d0efcb076b0ab7a7a4"); + let sample = hex!("5e5cd55c41f69080575d7999c25a5bfb"); + let mut header = hex!("4200bff4"); + let original = header; + let (first, packet_number) = header.split_at_mut(1); + + let protector = QuicHeaderProtector { + key: AeadKey::from(hp_key), + phantom: std::marker::PhantomData::, + }; + + let err = protector + .encrypt_in_place(&sample[..sample.len() - 1], &mut first[0], packet_number) + .expect_err("short sample must be rejected"); + + assert!(matches!(err, rustls::Error::General(_))); + assert_eq!(header, original); + } + + #[test] + fn tls13_decrypter_constructor_failure_returns_erroring_decrypter() { + let mut decrypter = Tls13AeadAlgorithm::decrypter( + &super::Aead::::DEFAULT, + AeadKey::from([0u8; 32]), + Iv::from([0u8; 12]), + ); + let mut payload = vec![0u8; 8]; + let msg = InboundOpaqueMessage::new( + ContentType::ApplicationData, + ProtocolVersion::TLSv1_3, + &mut payload, + ); + + let err = decrypter + .decrypt(msg, 0) + .expect_err("invalid constructor inputs should produce decrypt errors"); + + assert!(matches!(err, rustls::Error::DecryptError)); + } + + #[test] + fn quic_packet_key_constructor_failure_returns_erroring_key() { + let packet_key = super::Aead::::DEFAULT + .packet_key(AeadKey::from([0u8; 32]), Iv::from([0u8; 12])); + let mut payload = [0u8; 0]; + + let enc_err = packet_key.encrypt_in_place(0, &[], &mut payload); + assert!(matches!(enc_err, Err(rustls::Error::EncryptError))); + + let dec_err = packet_key + .decrypt_in_place(0, &[], &mut payload) + .expect_err("invalid constructor inputs should produce decrypt errors"); + assert!(matches!(dec_err, rustls::Error::DecryptError)); + } + + #[cfg(feature = "tls12")] + #[test] + fn tls12_decrypter_constructor_failure_returns_erroring_decrypter() { + let mut decrypter = Tls12AeadAlgorithm::decrypter( + &super::Aead::::DEFAULT, + AeadKey::from([0u8; 32]), + &[0u8; 4], + ); + let mut payload = vec![0u8; 8]; + let msg = InboundOpaqueMessage::new( + ContentType::ApplicationData, + ProtocolVersion::TLSv1_2, + &mut payload, + ); + + let err = decrypter + .decrypt(msg, 0) + .expect_err("invalid constructor inputs should produce decrypt errors"); + + assert!(matches!(err, rustls::Error::DecryptError)); + } + #[test] fn quic_chacha20_crypt() { // test vector from https://www.rfc-editor.org/rfc/rfc9001.html#name-chacha20-poly1305-short-hea @@ -616,7 +843,7 @@ mod tests { let protector = BoringAeadCrypter::::new( Iv::from(iv), &key, - rustls::ProtocolVersion::TLSv1_3, + ProtocolVersion::TLSv1_3, ) .unwrap(); @@ -635,4 +862,47 @@ mod tests { assert_eq!(cleartext, expected_cleartext); } + + #[test] + fn quic_decrypt_rejects_truncated_payload() { + let key = [0u8; ::KEY_SIZE]; + let iv = [0u8; 12]; + let crypter = BoringAeadCrypter::::new( + Iv::from(iv), + &key, + ProtocolVersion::TLSv1_3, + ) + .unwrap(); + + let mut payload = [0u8; ::TAG_LEN - 1]; + let err = crypter + .decrypt_in_place(0, &[], &mut payload) + .expect_err("truncated QUIC payload must fail decryption"); + + assert!(matches!(err, rustls::Error::DecryptError)); + } + + #[cfg(feature = "tls12")] + #[test] + fn tls12_decrypt_rejects_truncated_payload() { + let key = [0u8; ::KEY_SIZE]; + let iv = [0u8; 12]; + let mut decrypter = + BoringAeadCrypter::::new(Iv::from(iv), &key, ProtocolVersion::TLSv1_2).unwrap(); + + let min_payload_len = + ::EXPLICIT_NONCE_LEN + ::TAG_LEN; + let mut payload = vec![0u8; min_payload_len - 1]; + let msg = InboundOpaqueMessage::new( + ContentType::ApplicationData, + ProtocolVersion::TLSv1_2, + &mut payload, + ); + + let err = decrypter + .decrypt(msg, 0) + .expect_err("truncated TLS1.2 payload must fail decryption"); + + assert!(matches!(err, rustls::Error::DecryptError)); + } } diff --git a/boring-rustls-provider/src/aead/aes.rs b/boring-rustls-provider/src/aead/aes.rs index 03e0f3c..4fd591c 100644 --- a/boring-rustls-provider/src/aead/aes.rs +++ b/boring-rustls-provider/src/aead/aes.rs @@ -20,6 +20,7 @@ impl BoringCipher for Aes128 { const INTEGRITY_LIMIT: u64 = 1 << 52; const CONFIDENTIALITY_LIMIT: u64 = 1 << 23; + const FIPS_APPROVED: bool = true; fn new_cipher() -> Algorithm { Algorithm::aes_128_gcm() @@ -40,7 +41,7 @@ impl QuicCipher for Aes128 { const KEY_SIZE: usize = ::KEY_SIZE; const SAMPLE_LEN: usize = 16; - fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> [u8; 5] { + fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> Result<[u8; 5], rustls::Error> { quic_header_protection_mask::< { ::KEY_SIZE }, { ::SAMPLE_LEN }, @@ -65,6 +66,7 @@ impl BoringCipher for Aes256 { const INTEGRITY_LIMIT: u64 = 1 << 52; const CONFIDENTIALITY_LIMIT: u64 = 1 << 23; + const FIPS_APPROVED: bool = true; fn new_cipher() -> Algorithm { Algorithm::aes_256_gcm() @@ -85,7 +87,7 @@ impl QuicCipher for Aes256 { const KEY_SIZE: usize = ::KEY_SIZE; const SAMPLE_LEN: usize = 16; - fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> [u8; 5] { + fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> Result<[u8; 5], rustls::Error> { quic_header_protection_mask::< { ::KEY_SIZE }, { ::SAMPLE_LEN }, @@ -97,19 +99,32 @@ fn quic_header_protection_mask( cipher: boring::symm::Cipher, hp_key: &[u8], sample: &[u8], -) -> [u8; 5] { - assert!(hp_key.len() == KEY_SIZE); - assert!(sample.len() >= SAMPLE_LEN); +) -> Result<[u8; 5], rustls::Error> { + if hp_key.len() != KEY_SIZE { + return Err(rustls::Error::General( + "header protection key of invalid length".into(), + )); + } + if sample.len() != SAMPLE_LEN { + return Err(rustls::Error::General("sample of invalid length".into())); + } let mut output = [0u8; SAMPLE_LEN]; let mut crypter = boring::symm::Crypter::new(cipher, boring::symm::Mode::Encrypt, hp_key, None) - .expect("failed getting crypter"); + .map_err(|_| rustls::Error::General("failed generating header protection mask".into()))?; - let len = crypter.update(sample, &mut output).unwrap(); - let _ = len + crypter.finalize(&mut output[len..]).unwrap(); + let len = crypter + .update(sample, &mut output) + .map_err(|_| rustls::Error::General("failed generating header protection mask".into()))?; + let _total = len + + crypter.finalize(&mut output[len..]).map_err(|_| { + rustls::Error::General("failed generating header protection mask".into()) + })?; - output[..5].try_into().unwrap() + let mut mask = [0u8; 5]; + mask.copy_from_slice(&output[..5]); + Ok(mask) } #[cfg(test)] diff --git a/boring-rustls-provider/src/aead/chacha20.rs b/boring-rustls-provider/src/aead/chacha20.rs index c0af24c..142dd59 100644 --- a/boring-rustls-provider/src/aead/chacha20.rs +++ b/boring-rustls-provider/src/aead/chacha20.rs @@ -23,6 +23,7 @@ impl BoringCipher for ChaCha20Poly1305 { const INTEGRITY_LIMIT: u64 = 1 << 36; const CONFIDENTIALITY_LIMIT: u64 = u64::MAX; + const FIPS_APPROVED: bool = false; fn new_cipher() -> Algorithm { Algorithm::chacha20_poly1305() @@ -37,13 +38,21 @@ impl QuicCipher for ChaCha20Poly1305 { const KEY_SIZE: usize = 32; const SAMPLE_LEN: usize = 16; - fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> [u8; 5] { - assert!(hp_key.len() == ::KEY_SIZE); - assert!(sample.len() >= ::SAMPLE_LEN); + fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> Result<[u8; 5], rustls::Error> { + if hp_key.len() != ::KEY_SIZE { + return Err(rustls::Error::General( + "header protection key of invalid length".into(), + )); + } + if sample.len() != ::SAMPLE_LEN { + return Err(rustls::Error::General("sample of invalid length".into())); + } let mut mask = [0u8; 5]; // RFC9001 5.4.4: The first 4 bytes of the sampled ciphertext are the block counter. A ChaCha20 implementation could take a 32-bit integer in place of a byte sequence, in which case, the byte sequence is interpreted as a little-endian value. - let counter = u32::from_le_bytes(sample[0..4].try_into().unwrap()); + let mut counter_bytes = [0u8; 4]; + counter_bytes.copy_from_slice(&sample[0..4]); + let counter = u32::from_le_bytes(counter_bytes); // RFC9001 5.4.4: The remaining 12 bytes are used as the nonce. let nonce = &sample[4..16]; unsafe { @@ -56,7 +65,7 @@ impl QuicCipher for ChaCha20Poly1305 { counter, ); }; - mask + Ok(mask) } } @@ -95,7 +104,8 @@ mod tests { let sample = hex!("5e5cd55c41f69080575d7999c25a5bfb"); let hp_key = hex!("25a282b9e82f06f21f488917a4fc8f1b73573685608597d0efcb076b0ab7a7a4"); let expected_mask = hex!("aefefe7d03"); - let mask = ChaCha20Poly1305::header_protection_mask(&hp_key, &sample); + let mask = ChaCha20Poly1305::header_protection_mask(&hp_key, &sample) + .expect("valid QUIC sample/key should produce a mask"); assert_eq!(mask, expected_mask); } } diff --git a/boring-rustls-provider/src/hash.rs b/boring-rustls-provider/src/hash.rs index 44bad44..95ddc41 100644 --- a/boring-rustls-provider/src/hash.rs +++ b/boring-rustls-provider/src/hash.rs @@ -24,10 +24,14 @@ impl hash::Hash for Hash { boring::nid::Nid::SHA256 => hash::HashAlgorithm::SHA256, boring::nid::Nid::SHA384 => hash::HashAlgorithm::SHA384, boring::nid::Nid::SHA512 => hash::HashAlgorithm::SHA512, - _ => unimplemented!(), + _ => unreachable!("hash::Hash is only instantiated with SHA-2 digests"), } } + fn fips(&self) -> bool { + cfg!(feature = "fips") + } + fn output_len(&self) -> usize { MessageDigest::from_nid(self.0) .expect("failed getting digest") diff --git a/boring-rustls-provider/src/hkdf.rs b/boring-rustls-provider/src/hkdf.rs index cccaaf2..d4b070d 100644 --- a/boring-rustls-provider/src/hkdf.rs +++ b/boring-rustls-provider/src/hkdf.rs @@ -138,6 +138,10 @@ impl RustlsHkdf for Hkdf { } rustls::crypto::hmac::Tag::new(&hash[..hash_len as usize]) } + + fn fips(&self) -> bool { + cfg!(feature = "fips") + } } struct HkdfExpander { @@ -163,9 +167,17 @@ impl tls13::HkdfExpander for HkdfExpander { info: &[&[u8]], output: &mut [u8], ) -> Result<(), tls13::OutputLengthError> { + let max_output_len = self + .hash_len() + .checked_mul(255) + .ok_or(tls13::OutputLengthError)?; + if output.len() > max_output_len { + return Err(tls13::OutputLengthError); + } + let info_concat = info.concat(); unsafe { - boring_sys::HKDF_expand( + cvt(boring_sys::HKDF_expand( output.as_mut_ptr(), output.len(), self.digest.as_ptr(), @@ -173,8 +185,9 @@ impl tls13::HkdfExpander for HkdfExpander { self.prk_len, info_concat.as_ptr(), info_concat.len(), - ); - }; + )) + .map_err(|_| tls13::OutputLengthError)?; + } Ok(()) } @@ -201,3 +214,35 @@ impl tls13::HkdfExpander for HkdfExpander { self.digest.size() } } + +#[cfg(test)] +mod tests { + use boring::hash::MessageDigest; + use rustls::crypto::tls13::Hkdf as _; + + use super::{Hkdf, Sha256}; + + #[test] + fn expand_slice_rejects_output_larger_than_rfc_limit() { + let hkdf = Hkdf::::DEFAULT; + let expander = hkdf.extract_from_secret(None, b"ikm"); + let hash_len = MessageDigest::sha256().size(); + let mut output = vec![0u8; hash_len * 255 + 1]; + + assert!(expander.expand_slice(&[b"info"], &mut output).is_err()); + } + + #[test] + fn expand_slice_accepts_output_at_rfc_limit() { + let hkdf = Hkdf::::DEFAULT; + let expander = hkdf.extract_from_secret(None, b"ikm"); + let hash_len = MessageDigest::sha256().size(); + let mut output = vec![0u8; hash_len * 255]; + + expander + .expand_slice(&[b"info"], &mut output) + .expect("HKDF expand at RFC limit should succeed"); + + assert!(output.iter().any(|byte| *byte != 0)); + } +} diff --git a/boring-rustls-provider/src/hmac.rs b/boring-rustls-provider/src/hmac.rs index e0d5e49..fdcd508 100644 --- a/boring-rustls-provider/src/hmac.rs +++ b/boring-rustls-provider/src/hmac.rs @@ -20,16 +20,9 @@ struct BoringHmac(pub boring::nid::Nid); impl crypto::hmac::Hmac for BoringHmac { fn with_key(&self, key: &[u8]) -> Box { - let ctx = unsafe { - HmacCtx::from_ptr( - cvt_p(boring_sys::HMAC_CTX_new()).expect("failed getting hmac context"), - ) - }; - let md = MessageDigest::from_nid(self.0).expect("failed getting digest"); Box::new(BoringHmacKey { - ctx, md, key: Zeroizing::new(key.to_vec()), }) @@ -40,33 +33,35 @@ impl crypto::hmac::Hmac for BoringHmac { .expect("failed getting digest") .size() } + + fn fips(&self) -> bool { + cfg!(feature = "fips") + } } struct BoringHmacKey { - ctx: HmacCtx, md: MessageDigest, key: Zeroizing>, } impl BoringHmacKey { - fn init(&self) { + fn init(ctx: &HmacCtx, key: &[u8], md: MessageDigest) { unsafe { - // initialize a new hmac cvt(boring_sys::HMAC_Init_ex( - self.ctx.as_ptr(), - self.key.as_ptr() as *const c_void, - self.key.len(), - self.md.as_ptr(), + ctx.as_ptr(), + key.as_ptr() as *const c_void, + key.len(), + md.as_ptr(), ptr::null_mut(), )) } .expect("failed initializing hmac"); } - fn update(&self, bytes: &[u8]) { + fn update(ctx: &HmacCtx, bytes: &[u8]) { unsafe { cvt(boring_sys::HMAC_Update( - self.ctx.as_ptr(), + ctx.as_ptr(), bytes.as_ptr(), bytes.len(), )) @@ -74,11 +69,11 @@ impl BoringHmacKey { .expect("failed updating hmac"); } - fn finish(&self, out: &mut [u8]) -> usize { + fn finish(ctx: &HmacCtx, out: &mut [u8]) -> usize { let mut out_len = 0; unsafe { cvt(boring_sys::HMAC_Final( - self.ctx.as_ptr(), + ctx.as_ptr(), out.as_mut_ptr(), &mut out_len, )) @@ -86,21 +81,28 @@ impl BoringHmacKey { .expect("failed hmac final"); out_len as usize } + + fn new_ctx() -> HmacCtx { + unsafe { + HmacCtx::from_ptr(cvt_p(boring_sys::HMAC_CTX_new()).expect("failed creating HMAC_CTX")) + } + } } impl crypto::hmac::Key for BoringHmacKey { fn sign_concat(&self, first: &[u8], middle: &[&[u8]], last: &[u8]) -> crypto::hmac::Tag { - self.init(); + let ctx = Self::new_ctx(); + Self::init(&ctx, self.key.as_slice(), self.md); - self.update(first); + Self::update(&ctx, first); for m in middle { - self.update(m); + Self::update(&ctx, m); } - self.update(last); + Self::update(&ctx, last); let mut out = Zeroizing::new([0u8; boring_sys::EVP_MAX_MD_SIZE as usize]); - let out_len = self.finish(&mut out[..]); + let out_len = Self::finish(&ctx, &mut out[..]); crypto::hmac::Tag::new(&out[..out_len]) } diff --git a/boring-rustls-provider/src/kx/ex.rs b/boring-rustls-provider/src/kx/ex.rs index 5e80694..297844b 100644 --- a/boring-rustls-provider/src/kx/ex.rs +++ b/boring-rustls-provider/src/kx/ex.rs @@ -5,6 +5,7 @@ use boring::{ ec::{EcGroup, EcKey}, error::ErrorStack, nid::Nid, + pkey::Id, pkey::{PKey, PKeyRef, Private}, }; #[cfg(not(feature = "fips"))] @@ -12,7 +13,6 @@ use boring_additions::evp::EvpPkeyCtx; #[cfg(not(feature = "fips"))] use foreign_types::ForeignType; use rustls::crypto; -use spki::der::Decode; use crate::helper::log_and_map; #[cfg(not(feature = "fips"))] @@ -97,18 +97,24 @@ impl KeyExchange { /// Decodes a SPKI public key to it's raw public key component fn raw_public_key(pkey: &PKeyRef) -> Result, ErrorStack> { - let spki = pkey.public_key_to_der()?; + if pkey.id() == Id::EC { + let ec_key = pkey.ec_key()?; + let mut bn_ctx = boring::bn::BigNumContext::new()?; - // parse the key - let pkey = spki::SubjectPublicKeyInfoRef::from_der(spki.as_ref()) - .expect("failed parsing spki bytes"); + return ec_key.public_key().to_bytes( + ec_key.group(), + boring::ec::PointConversionForm::UNCOMPRESSED, + &mut bn_ctx, + ); + } - // return the raw public key as a new vec - Ok(Vec::from( - pkey.subject_public_key - .as_bytes() - .expect("failed getting raw spki bytes"), - )) + let mut output = vec![0u8; pkey.raw_public_key_len()?]; + let used_len = { + let used = pkey.raw_public_key(&mut output)?; + used.len() + }; + output.truncate(used_len); + Ok(output) } /// Derives a shared secret using the peer's raw public key diff --git a/boring-rustls-provider/src/kx/mod.rs b/boring-rustls-provider/src/kx/mod.rs index e38c295..44b524c 100644 --- a/boring-rustls-provider/src/kx/mod.rs +++ b/boring-rustls-provider/src/kx/mod.rs @@ -32,6 +32,10 @@ impl crypto::SupportedKxGroup for X25519 { fn name(&self) -> rustls::NamedGroup { rustls::NamedGroup::X25519 } + + fn fips(&self) -> bool { + false + } } /// A secp256r1-based key exchange @@ -48,6 +52,10 @@ impl crypto::SupportedKxGroup for Secp256r1 { fn name(&self) -> rustls::NamedGroup { rustls::NamedGroup::secp256r1 } + + fn fips(&self) -> bool { + cfg!(feature = "fips") + } } /// A secp384r1-based key exchange @@ -64,4 +72,8 @@ impl crypto::SupportedKxGroup for Secp384r1 { fn name(&self) -> rustls::NamedGroup { rustls::NamedGroup::secp384r1 } + + fn fips(&self) -> bool { + cfg!(feature = "fips") + } } diff --git a/boring-rustls-provider/src/lib.rs b/boring-rustls-provider/src/lib.rs index 6908297..e02da06 100644 --- a/boring-rustls-provider/src/lib.rs +++ b/boring-rustls-provider/src/lib.rs @@ -1,6 +1,8 @@ use std::sync::Arc; use helper::log_and_map; +#[cfg(all(feature = "fips", feature = "log"))] +use log::warn; use rustls::{ crypto::{CryptoProvider, GetRandomFailed, SupportedKxGroup}, SupportedCipherSuite, @@ -33,6 +35,25 @@ pub fn provider() -> CryptoProvider { } pub fn provider_with_ciphers(ciphers: Vec) -> CryptoProvider { + #[cfg(feature = "fips")] + let ciphers = { + let original_len = ciphers.len(); + let filtered = ciphers + .into_iter() + .filter(|suite| ALL_FIPS_CIPHER_SUITES.contains(suite)) + .collect::>(); + + if filtered.len() != original_len { + #[cfg(feature = "log")] + warn!( + "filtered {} non-FIPS cipher suite(s) from provider_with_ciphers", + original_len - filtered.len() + ); + } + + filtered + }; + CryptoProvider { cipher_suites: ciphers, #[cfg(feature = "fips")] @@ -55,6 +76,10 @@ impl rustls::crypto::SecureRandom for Provider { fn fill(&self, bytes: &mut [u8]) -> Result<(), rustls::crypto::GetRandomFailed> { boring::rand::rand_bytes(bytes).map_err(|e| log_and_map("rand_bytes", e, GetRandomFailed)) } + + fn fips(&self) -> bool { + cfg!(feature = "fips") + } } impl rustls::crypto::KeyProvider for Provider { @@ -64,6 +89,10 @@ impl rustls::crypto::KeyProvider for Provider { ) -> Result, rustls::Error> { sign::BoringPrivateKey::try_from(key_der).map(|x| Arc::new(x) as _) } + + fn fips(&self) -> bool { + cfg!(feature = "fips") + } } #[cfg(feature = "fips")] diff --git a/boring-rustls-provider/src/prf.rs b/boring-rustls-provider/src/prf.rs index ca174f5..ca5fd35 100644 --- a/boring-rustls-provider/src/prf.rs +++ b/boring-rustls-provider/src/prf.rs @@ -29,6 +29,10 @@ impl crypto::tls12::Prf for PrfTls1WithDigest { let digest = boring::hash::MessageDigest::from_nid(self.0).expect("failed getting digest"); prf(digest, output, secret, label, seed).expect("failed calculating prf") } + + fn fips(&self) -> bool { + cfg!(feature = "fips") + } } fn prf( diff --git a/boring-rustls-provider/src/sign.rs b/boring-rustls-provider/src/sign.rs index 41a2ead..819b49a 100644 --- a/boring-rustls-provider/src/sign.rs +++ b/boring-rustls-provider/src/sign.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use boring::{ hash::MessageDigest, + nid::Nid, pkey::{Id, PKeyRef, Private}, rsa::Padding, sign::{RsaPssSaltlen, Signer}, @@ -20,16 +21,28 @@ const ALL_RSA_SCHEMES: &[SignatureScheme] = &[ SignatureScheme::RSA_PKCS1_SHA256, ]; -const ALL_EC_SCHEMES: &[SignatureScheme] = &[ - SignatureScheme::ECDSA_NISTP256_SHA256, - SignatureScheme::ECDSA_NISTP384_SHA384, - SignatureScheme::ECDSA_NISTP521_SHA512, -]; +const EC_P256_SCHEMES: &[SignatureScheme] = &[SignatureScheme::ECDSA_NISTP256_SHA256]; +const EC_P384_SCHEMES: &[SignatureScheme] = &[SignatureScheme::ECDSA_NISTP384_SHA384]; +const EC_P521_SCHEMES: &[SignatureScheme] = &[SignatureScheme::ECDSA_NISTP521_SHA512]; -/// An abstraction over a boringssl private key -/// used for signing +#[derive(Debug, Clone, Copy)] +enum EcCurve { + P256, + P384, + P521, +} + +#[derive(Debug, Clone, Copy)] +enum KeyKind { + Rsa, + Ec(EcCurve), + Ed25519, + Ed448, +} + +/// An abstraction over a boringssl private key used for signing. #[derive(Debug)] -pub struct BoringPrivateKey(Arc>, rustls::SignatureAlgorithm); +pub struct BoringPrivateKey(Arc>, KeyKind); impl TryFrom> for BoringPrivateKey { type Error = rustls::Error; @@ -37,25 +50,83 @@ impl TryFrom> for BoringPrivateKey { fn try_from(value: PrivateKeyDer<'static>) -> Result { let pkey = match value { PrivateKeyDer::Pkcs8(der) => { - boring::pkey::PKey::private_key_from_pkcs8(der.secret_pkcs8_der()) - .map_err(|e| log_and_map("private_key_from_pkcs8", e, ())) + boring::pkey::PKey::private_key_from_pkcs8(der.secret_pkcs8_der()).map_err(|e| { + log_and_map( + "private_key_from_pkcs8", + e, + rustls::Error::General("failed loading private key".into()), + ) + }) } PrivateKeyDer::Pkcs1(der) => { - boring::pkey::PKey::private_key_from_der(der.secret_pkcs1_der()) - .map_err(|e| log_and_map("private_key_from_der", e, ())) + boring::pkey::PKey::private_key_from_der(der.secret_pkcs1_der()).map_err(|e| { + log_and_map( + "private_key_from_der_pkcs1", + e, + rustls::Error::General("failed loading private key".into()), + ) + }) } - _ => Err(()), - } - .map_err(|_| rustls::Error::General("failed loading private key".into()))?; + PrivateKeyDer::Sec1(der) => { + boring::pkey::PKey::private_key_from_der(der.secret_sec1_der()).map_err(|e| { + log_and_map( + "private_key_from_der_sec1", + e, + rustls::Error::General("failed loading private key".into()), + ) + }) + } + _ => { + return Err(rustls::Error::General( + "unsupported private key encoding".into(), + )); + } + }?; - let sig = match pkey.id() { - Id::RSA => rustls::SignatureAlgorithm::RSA, - Id::EC => rustls::SignatureAlgorithm::ECDSA, - Id::ED25519 => rustls::SignatureAlgorithm::ED25519, - Id::ED448 => rustls::SignatureAlgorithm::ED448, + let kind = match pkey.id() { + Id::RSA => KeyKind::Rsa, + Id::EC => { + let ec_key = pkey.ec_key().map_err(|e| { + log_and_map( + "ec_key", + e, + rustls::Error::General("failed loading EC private key".into()), + ) + })?; + + let curve_nid = ec_key.group().curve_name().ok_or_else(|| { + rustls::Error::General("unsupported EC key without named curve".into()) + })?; + + let curve = match curve_nid { + Nid::X9_62_PRIME256V1 => EcCurve::P256, + Nid::SECP384R1 => EcCurve::P384, + Nid::SECP521R1 => EcCurve::P521, + _ => { + return Err(rustls::Error::General( + "unsupported EC private key curve".into(), + )); + } + }; + + KeyKind::Ec(curve) + } + Id::ED25519 => KeyKind::Ed25519, + Id::ED448 => KeyKind::Ed448, _ => return Err(rustls::Error::General("unsupported key format".into())), }; - Ok(Self(Arc::new(pkey), sig)) + + #[cfg(feature = "fips")] + match kind { + KeyKind::Rsa | KeyKind::Ec(EcCurve::P256 | EcCurve::P384) => {} + KeyKind::Ec(EcCurve::P521) | KeyKind::Ed25519 | KeyKind::Ed448 => { + return Err(rustls::Error::General( + "key type is not allowed in FIPS mode".into(), + )); + } + } + + Ok(Self(Arc::new(pkey), kind)) } } @@ -63,75 +134,105 @@ fn rsa_signer_from_params( key: &PKeyRef, digest: MessageDigest, padding: Padding, -) -> Signer<'_> { - let mut signer = Signer::new(digest, key).expect("failed getting signer"); - signer - .set_rsa_padding(padding) - .expect("failed setting padding"); +) -> Result, rustls::Error> { + let mut signer = Signer::new(digest, key).map_err(|e| { + log_and_map( + "Signer::new", + e, + rustls::Error::General("failed preparing signer".into()), + ) + })?; + + signer.set_rsa_padding(padding).map_err(|e| { + log_and_map( + "set_rsa_padding", + e, + rustls::Error::General("failed preparing signer".into()), + ) + })?; + if padding == Padding::PKCS1_PSS { signer .set_rsa_pss_saltlen(RsaPssSaltlen::DIGEST_LENGTH) - .expect("failed setting rsa_pss salt lengths"); - signer - .set_rsa_mgf1_md(digest) - .expect("failed setting mgf1 digest"); + .map_err(|e| { + log_and_map( + "set_rsa_pss_saltlen", + e, + rustls::Error::General("failed preparing signer".into()), + ) + })?; + + signer.set_rsa_mgf1_md(digest).map_err(|e| { + log_and_map( + "set_rsa_mgf1_md", + e, + rustls::Error::General("failed preparing signer".into()), + ) + })?; } - signer + Ok(signer) } -fn ec_signer_from_params(key: &PKeyRef, digest: MessageDigest) -> Signer<'_> { - let signer = Signer::new(digest, key).expect("failed getting signer"); - signer +fn ec_signer_from_params( + key: &PKeyRef, + digest: MessageDigest, +) -> Result, rustls::Error> { + Signer::new(digest, key).map_err(|e| { + log_and_map( + "Signer::new", + e, + rustls::Error::General("failed preparing signer".into()), + ) + }) } -impl BoringPrivateKey {} - impl SigningKey for BoringPrivateKey { fn choose_scheme( &self, offered: &[rustls::SignatureScheme], ) -> Option> { - match self.1 { - rustls::SignatureAlgorithm::RSA => ALL_RSA_SCHEMES + let scheme = match self.1 { + KeyKind::Rsa => ALL_RSA_SCHEMES .iter() - .find(|scheme| offered.contains(scheme)) - .map(|&scheme| Box::new(BoringSigner(self.0.clone(), scheme)) as _), - rustls::SignatureAlgorithm::ECDSA => ALL_EC_SCHEMES + .find(|scheme| offered.contains(scheme)), + KeyKind::Ec(EcCurve::P256) => EC_P256_SCHEMES .iter() - .find(|scheme| offered.contains(scheme)) - .map(|&scheme| Box::new(BoringSigner(self.0.clone(), scheme)) as _), - rustls::SignatureAlgorithm::ED25519 - if offered.contains(&rustls::SignatureScheme::ED25519) => - { - Some(Box::new(BoringSigner( - self.0.clone(), - rustls::SignatureScheme::ED25519, - ))) + .find(|scheme| offered.contains(scheme)), + KeyKind::Ec(EcCurve::P384) => EC_P384_SCHEMES + .iter() + .find(|scheme| offered.contains(scheme)), + KeyKind::Ec(EcCurve::P521) => EC_P521_SCHEMES + .iter() + .find(|scheme| offered.contains(scheme)), + KeyKind::Ed25519 if offered.contains(&rustls::SignatureScheme::ED25519) => { + Some(&rustls::SignatureScheme::ED25519) } - rustls::SignatureAlgorithm::ED448 - if offered.contains(&rustls::SignatureScheme::ED448) => - { - Some(Box::new(BoringSigner( - self.0.clone(), - rustls::SignatureScheme::ED448, - ))) + KeyKind::Ed448 if offered.contains(&rustls::SignatureScheme::ED448) => { + Some(&rustls::SignatureScheme::ED448) } _ => None, - } + }?; + + Some(Box::new(BoringSigner(self.0.clone(), *scheme))) } fn algorithm(&self) -> rustls::SignatureAlgorithm { - self.1 + match self.1 { + KeyKind::Rsa => rustls::SignatureAlgorithm::RSA, + KeyKind::Ec(_) => rustls::SignatureAlgorithm::ECDSA, + KeyKind::Ed25519 => rustls::SignatureAlgorithm::ED25519, + KeyKind::Ed448 => rustls::SignatureAlgorithm::ED448, + } } } -/// A boringssl-based Signer +/// A boringssl-based Signer. #[derive(Debug)] pub struct BoringSigner(Arc>, rustls::SignatureScheme); impl BoringSigner { - fn get_signer(&self) -> Signer<'_> { + fn get_signer(&self) -> Result, rustls::Error> { match self.1 { SignatureScheme::RSA_PKCS1_SHA256 => { rsa_signer_from_params(self.0.as_ref(), MessageDigest::sha256(), Padding::PKCS1) @@ -142,7 +243,6 @@ impl BoringSigner { SignatureScheme::RSA_PKCS1_SHA512 => { rsa_signer_from_params(self.0.as_ref(), MessageDigest::sha512(), Padding::PKCS1) } - SignatureScheme::RSA_PSS_SHA256 => { rsa_signer_from_params(self.0.as_ref(), MessageDigest::sha256(), Padding::PKCS1_PSS) } @@ -152,7 +252,6 @@ impl BoringSigner { SignatureScheme::RSA_PSS_SHA512 => { rsa_signer_from_params(self.0.as_ref(), MessageDigest::sha512(), Padding::PKCS1_PSS) } - SignatureScheme::ECDSA_NISTP256_SHA256 => { ec_signer_from_params(self.0.as_ref(), MessageDigest::sha256()) } @@ -162,19 +261,25 @@ impl BoringSigner { SignatureScheme::ECDSA_NISTP521_SHA512 => { ec_signer_from_params(self.0.as_ref(), MessageDigest::sha512()) } - SignatureScheme::ED25519 | SignatureScheme::ED448 => { - Signer::new_without_digest(self.0.as_ref()).expect("failed getting signer") + Signer::new_without_digest(self.0.as_ref()).map_err(|e| { + log_and_map( + "Signer::new_without_digest", + e, + rustls::Error::General("failed preparing signer".into()), + ) + }) } - - _ => unimplemented!(), + _ => Err(rustls::Error::General( + "unsupported signature scheme for private key".into(), + )), } } } impl rustls::sign::Signer for BoringSigner { fn sign(&self, message: &[u8]) -> Result, rustls::Error> { - let mut signer = self.get_signer(); + let mut signer = self.get_signer()?; let max_sig_len = signer .len() .map_err(|e| log_and_map("len", e, rustls::Error::General("failed signing".into())))?; @@ -195,3 +300,75 @@ impl rustls::sign::Signer for BoringSigner { self.1 } } + +#[cfg(test)] +mod tests { + use boring::{ + ec::{EcGroup, EcKey}, + nid::Nid, + pkey::{PKey, Private}, + rsa::Rsa, + }; + use rustls::sign::SigningKey; + use rustls::{SignatureAlgorithm, SignatureScheme}; + use rustls_pki_types::{PrivateKeyDer, PrivatePkcs8KeyDer, PrivateSec1KeyDer}; + + use super::BoringPrivateKey; + + fn p256_private_key() -> PKey { + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let ec_key = EcKey::generate(&group).unwrap(); + PKey::from_ec_key(ec_key).unwrap() + } + + #[test] + fn loads_sec1_ec_private_key() { + let pkey = p256_private_key(); + let sec1_der = pkey.private_key_to_der().unwrap(); + let key_der = PrivateKeyDer::Sec1(PrivateSec1KeyDer::from(sec1_der)); + + let key = BoringPrivateKey::try_from(key_der).expect("SEC1 private key should load"); + + assert_eq!(key.algorithm(), SignatureAlgorithm::ECDSA); + } + + #[test] + fn p256_key_chooses_only_p256_scheme() { + let pkey = p256_private_key(); + let pkcs8_der = pkey.private_key_to_der_pkcs8().unwrap(); + let key_der = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(pkcs8_der)); + + let key = BoringPrivateKey::try_from(key_der).expect("P-256 private key should load"); + + let offered = [ + SignatureScheme::ECDSA_NISTP384_SHA384, + SignatureScheme::ECDSA_NISTP256_SHA256, + ]; + let signer = key + .choose_scheme(&offered) + .expect("P-256 key should select P-256 scheme"); + assert_eq!(signer.scheme(), SignatureScheme::ECDSA_NISTP256_SHA256); + + assert!(key + .choose_scheme(&[SignatureScheme::ECDSA_NISTP384_SHA384]) + .is_none()); + } + + #[test] + fn rsa_key_prefers_pss_when_available() { + let rsa = Rsa::generate(2048).unwrap(); + let pkey = PKey::from_rsa(rsa).unwrap(); + let pkcs8_der = pkey.private_key_to_der_pkcs8().unwrap(); + let key_der = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(pkcs8_der)); + + let key = BoringPrivateKey::try_from(key_der).expect("RSA private key should load"); + let signer = key + .choose_scheme(&[ + SignatureScheme::RSA_PKCS1_SHA256, + SignatureScheme::RSA_PSS_SHA256, + ]) + .expect("RSA key should select an offered scheme"); + + assert_eq!(signer.scheme(), SignatureScheme::RSA_PSS_SHA256); + } +} diff --git a/boring-rustls-provider/src/verify/ec.rs b/boring-rustls-provider/src/verify/ec.rs index f8984e3..34c8a3d 100644 --- a/boring-rustls-provider/src/verify/ec.rs +++ b/boring-rustls-provider/src/verify/ec.rs @@ -40,7 +40,7 @@ impl SignatureVerificationAlgorithm for BoringEcVerifier { ec_verifier_from_params(public_key.as_ref(), MessageDigest::sha512()) } - _ => unimplemented!(), + _ => return Err(InvalidSignature), } .map_err(|e| helper::log_and_map("ec_verifier_from_params", e, InvalidSignature))?; @@ -61,7 +61,7 @@ impl SignatureVerificationAlgorithm for BoringEcVerifier { 0x04, 0x00, 0x23, ]) } - _ => unimplemented!(), + _ => unreachable!("BoringEcVerifier only supports configured ECDSA schemes"), } } @@ -75,9 +75,13 @@ impl SignatureVerificationAlgorithm for BoringEcVerifier { 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x04, ]) } - _ => unimplemented!(), + _ => unreachable!("BoringEcVerifier only supports configured ECDSA schemes"), } } + + fn fips(&self) -> bool { + cfg!(feature = "fips") && !matches!(self.0, SignatureScheme::ECDSA_NISTP521_SHA512) + } } fn ec_verifier_from_params( @@ -92,7 +96,7 @@ fn group_for_scheme(scheme: SignatureScheme) -> Result boring::nid::Nid::X9_62_PRIME256V1, SignatureScheme::ECDSA_NISTP384_SHA384 => boring::nid::Nid::SECP384R1, SignatureScheme::ECDSA_NISTP521_SHA512 => boring::nid::Nid::SECP521R1, - _ => unimplemented!(), + _ => unreachable!("BoringEcVerifier only supports configured ECDSA schemes"), }; boring::ec::EcGroup::from_curve_name(nid) } diff --git a/boring-rustls-provider/src/verify/ed.rs b/boring-rustls-provider/src/verify/ed.rs index e7cb81e..3eeeb26 100644 --- a/boring-rustls-provider/src/verify/ed.rs +++ b/boring-rustls-provider/src/verify/ed.rs @@ -46,9 +46,13 @@ impl SignatureVerificationAlgorithm for BoringEdVerifier { // rfc8410#section-3: 1.3.101.113: -> DER: 06 03 2B 65 71 rustls_pki_types::AlgorithmIdentifier::from_slice(&[0x06, 0x03, 0x2B, 0x65, 0x71]) } - _ => unimplemented!(), + _ => unreachable!("BoringEdVerifier only supports configured EdDSA schemes"), } } + + fn fips(&self) -> bool { + false + } } fn ed_verifier_from_params( @@ -64,7 +68,7 @@ fn ed_public_key_for_scheme( let nid = boring::nid::Nid::from_raw(match scheme { SignatureScheme::ED25519 => boring_sys::EVP_PKEY_ED25519, SignatureScheme::ED448 => boring_sys::EVP_PKEY_ED448, - _ => unimplemented!(), + _ => return Err(ErrorStack::get()), }); public_key(spki_spk, nid) } diff --git a/boring-rustls-provider/src/verify/rsa.rs b/boring-rustls-provider/src/verify/rsa.rs index 5691369..e41d494 100644 --- a/boring-rustls-provider/src/verify/rsa.rs +++ b/boring-rustls-provider/src/verify/rsa.rs @@ -64,8 +64,8 @@ impl SignatureVerificationAlgorithm for BoringRsaVerifier { Padding::PKCS1_PSS, ), - _ => unimplemented!(), - }; + _ => return Err(InvalidSignature), + }?; verifier.verify_oneshot(signature, message).map_or_else( |_| Err(InvalidSignature), |res| if res { Ok(()) } else { Err(InvalidSignature) }, @@ -86,30 +86,35 @@ impl SignatureVerificationAlgorithm for BoringRsaVerifier { SignatureScheme::RSA_PSS_SHA384 => alg_id::RSA_PSS_SHA384, SignatureScheme::RSA_PSS_SHA512 => alg_id::RSA_PSS_SHA512, - _ => unimplemented!(), + _ => unreachable!("BoringRsaVerifier only supports configured RSA schemes"), } } + + fn fips(&self) -> bool { + cfg!(feature = "fips") + } } fn rsa_verifier_from_params( key: &boring::pkey::PKeyRef, digest: MessageDigest, padding: Padding, -) -> boring::sign::Verifier<'_> { - let mut verifier = boring::sign::Verifier::new(digest, key).expect("failed getting verifier"); +) -> Result, InvalidSignature> { + let mut verifier = boring::sign::Verifier::new(digest, key) + .map_err(|e| log_and_map("Verifier::new", e, InvalidSignature))?; verifier .set_rsa_padding(padding) - .expect("failed setting padding"); + .map_err(|e| log_and_map("set_rsa_padding", e, InvalidSignature))?; if padding == Padding::PKCS1_PSS { verifier .set_rsa_pss_saltlen(RsaPssSaltlen::DIGEST_LENGTH) - .expect("failed setting rsa_pss salt lengths"); + .map_err(|e| log_and_map("set_rsa_pss_saltlen", e, InvalidSignature))?; verifier .set_rsa_mgf1_md(digest) - .expect("failed setting mgf1 digest"); + .map_err(|e| log_and_map("set_rsa_mgf1_md", e, InvalidSignature))?; } - verifier + Ok(verifier) } pub(crate) fn decode_spki_spk( diff --git a/boring-rustls-provider/tests/e2e.rs b/boring-rustls-provider/tests/e2e.rs index fa5bd5a..c53616e 100644 --- a/boring-rustls-provider/tests/e2e.rs +++ b/boring-rustls-provider/tests/e2e.rs @@ -5,8 +5,6 @@ use tokio::{ net::TcpStream, }; -#[cfg(feature = "tls12")] -use boring_rustls_provider::tls12; use boring_rustls_provider::tls13; #[cfg(feature = "tls12")] use rustls::version::TLS12; @@ -15,6 +13,63 @@ use rustls_pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}; use tokio::net::TcpListener; use tokio_rustls::{TlsAcceptor, TlsConnector}; +fn tls13_provider_suites() -> Vec { + boring_rustls_provider::provider() + .cipher_suites + .into_iter() + .filter(|suite| matches!(suite, SupportedCipherSuite::Tls13(_))) + .collect() +} + +#[cfg(feature = "tls12")] +fn tls12_provider_suites_for_ecdsa() -> Vec { + boring_rustls_provider::provider() + .cipher_suites + .into_iter() + .filter(|suite| { + let SupportedCipherSuite::Tls12(suite) = suite else { + return false; + }; + + suite.sign.iter().any(|scheme| { + matches!( + scheme, + rustls::SignatureScheme::ECDSA_NISTP256_SHA256 + | rustls::SignatureScheme::ECDSA_NISTP384_SHA384 + | rustls::SignatureScheme::ECDSA_NISTP521_SHA512 + | rustls::SignatureScheme::ED25519 + | rustls::SignatureScheme::ED448 + ) + }) + }) + .collect() +} + +#[cfg(feature = "tls12")] +fn tls12_provider_suites_for_rsa() -> Vec { + boring_rustls_provider::provider() + .cipher_suites + .into_iter() + .filter(|suite| { + let SupportedCipherSuite::Tls12(suite) = suite else { + return false; + }; + + suite.sign.iter().any(|scheme| { + matches!( + scheme, + rustls::SignatureScheme::RSA_PKCS1_SHA256 + | rustls::SignatureScheme::RSA_PKCS1_SHA384 + | rustls::SignatureScheme::RSA_PKCS1_SHA512 + | rustls::SignatureScheme::RSA_PSS_SHA256 + | rustls::SignatureScheme::RSA_PSS_SHA384 + | rustls::SignatureScheme::RSA_PSS_SHA512 + ) + }) + }) + .collect() +} + #[tokio::test] async fn test_tls13_crypto() { let pki = TestPki::new(&rcgen::PKCS_ECDSA_P256_SHA256); @@ -22,17 +77,7 @@ async fn test_tls13_crypto() { let root_store = pki.client_root_store(); let server_config = pki.server_config(); - #[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_256_GCM_SHA384), - SupportedCipherSuite::Tls13(&tls13::CHACHA20_POLY1305_SHA256), - ]; + let ciphers = tls13_provider_suites(); for cipher in ciphers { let config = ClientConfig::builder_with_provider(Arc::new( @@ -47,6 +92,43 @@ async fn test_tls13_crypto() { } } +#[test] +fn provider_kx_groups_reject_invalid_peer_keys_without_panicking() { + for group in boring_rustls_provider::provider().kx_groups { + let kx = group.start().expect("provider KX group should initialize"); + + let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| kx.complete(&[]))); + assert!(outcome.is_ok(), "KX group {:?} panicked", group.name()); + assert!( + outcome.expect("already checked for panic").is_err(), + "KX group {:?} accepted an invalid key share", + group.name() + ); + } +} + +#[test] +fn provider_verifiers_reject_malformed_inputs_without_panicking() { + let provider = boring_rustls_provider::provider(); + + for (index, verifier) in provider + .signature_verification_algorithms + .all + .iter() + .enumerate() + { + let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + verifier.verify_signature(&[], b"message", &[]) + })); + + assert!(outcome.is_ok(), "verifier #{index} panicked"); + assert!( + outcome.expect("already checked for panic").is_err(), + "verifier #{index} accepted malformed inputs" + ); + } +} + /// Self-to-self TLS 1.3 handshake using only the X25519MLKEM768 PQ hybrid group. #[cfg(feature = "mlkem")] #[tokio::test] @@ -227,6 +309,25 @@ fn fips_provider_excludes_chacha20_cipher_suites() { } } +#[test] +#[cfg(feature = "fips")] +fn fips_provider_with_ciphers_filters_non_fips_input() { + use rustls::CipherSuite; + + let provider = boring_rustls_provider::provider_with_ciphers(vec![ + SupportedCipherSuite::Tls13(&tls13::CHACHA20_POLY1305_SHA256), + SupportedCipherSuite::Tls13(&tls13::AES_128_GCM_SHA256), + ]); + + let suites = provider + .cipher_suites + .iter() + .map(|suite| suite.suite()) + .collect::>(); + + assert_eq!(suites, vec![CipherSuite::TLS13_AES_128_GCM_SHA256]); +} + #[test] #[cfg(feature = "fips")] fn fips_provider_restricts_kx_groups() { @@ -310,6 +411,40 @@ fn non_fips_provider_keeps_non_fips_algorithms() { .any(|group| group.name() == NamedGroup::X25519)); } +#[test] +#[cfg(not(feature = "fips"))] +fn non_fips_provider_components_report_non_fips() { + let provider = boring_rustls_provider::provider(); + + assert!(!provider.secure_random.fips()); + assert!(!provider.key_provider.fips()); +} + +#[test] +#[cfg(not(feature = "fips"))] +fn non_fips_provider_with_ciphers_keeps_requested_suites() { + use rustls::CipherSuite; + + let provider = boring_rustls_provider::provider_with_ciphers(vec![ + SupportedCipherSuite::Tls13(&tls13::CHACHA20_POLY1305_SHA256), + SupportedCipherSuite::Tls13(&tls13::AES_128_GCM_SHA256), + ]); + + let suites = provider + .cipher_suites + .iter() + .map(|suite| suite.suite()) + .collect::>(); + + assert_eq!( + suites, + vec![ + CipherSuite::TLS13_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS13_AES_128_GCM_SHA256, + ] + ); +} + #[test] #[cfg(all(not(feature = "fips"), feature = "mlkem"))] fn non_fips_provider_includes_pq_group() { @@ -347,17 +482,7 @@ async fn test_tls12_ec_crypto() { let root_store = pki.client_root_store(); let server_config = pki.server_config(); - #[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_AES256_GCM_SHA384), - SupportedCipherSuite::Tls12(&tls12::ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256), - ]; + let ciphers = tls12_provider_suites_for_ecdsa(); for cipher in ciphers { let config = ClientConfig::builder_with_provider(Arc::new( @@ -380,17 +505,7 @@ async fn test_tls12_rsa_crypto() { let root_store = pki.client_root_store(); let server_config = pki.server_config(); - #[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_AES256_GCM_SHA384), - SupportedCipherSuite::Tls12(&tls12::ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256), - ]; + let ciphers = tls12_provider_suites_for_rsa(); for cipher in ciphers { let config = ClientConfig::builder_with_provider(Arc::new( diff --git a/boring-rustls-provider/tests/panic_surface.rs b/boring-rustls-provider/tests/panic_surface.rs new file mode 100644 index 0000000..3a960f4 --- /dev/null +++ b/boring-rustls-provider/tests/panic_surface.rs @@ -0,0 +1,244 @@ +use std::fs; +use std::path::{Path, PathBuf}; + +const BANNED_TOKENS: &[&str] = &[ + "unwrap(", + "expect(", + "assert!(", + "assert_eq!(", + "assert_ne!(", + "panic!(", + "unreachable!(", + "unimplemented!(", +]; + +struct AllowlistedPanic { + path_suffix: &'static str, + line_fragment: &'static str, + reason: &'static str, +} + +const ALLOWLIST: &[AllowlistedPanic] = &[ + AllowlistedPanic { + path_suffix: "boring-rustls-provider/src/hash.rs", + line_fragment: "failed getting hash digest", + reason: "rustls hash trait is infallible; provider currently cannot surface this as Result", + }, + AllowlistedPanic { + path_suffix: "boring-rustls-provider/src/hash.rs", + line_fragment: "failed getting hasher", + reason: "rustls hash trait is infallible; provider currently cannot surface this as Result", + }, + AllowlistedPanic { + path_suffix: "boring-rustls-provider/src/hash.rs", + line_fragment: "hash::Hash is only instantiated with SHA-2 digests", + reason: "constructor invariant over static hash-provider constants", + }, + AllowlistedPanic { + path_suffix: "boring-rustls-provider/src/hash.rs", + line_fragment: "failed getting digest", + reason: "rustls hash trait is infallible; provider currently cannot surface this as Result", + }, + AllowlistedPanic { + path_suffix: "boring-rustls-provider/src/hash.rs", + line_fragment: "failed finishing hash", + reason: "rustls hash trait is infallible; provider currently cannot surface this as Result", + }, + AllowlistedPanic { + path_suffix: "boring-rustls-provider/src/hash.rs", + line_fragment: "failed adding data to hash", + reason: "rustls hash trait is infallible; provider currently cannot surface this as Result", + }, + AllowlistedPanic { + path_suffix: "boring-rustls-provider/src/hkdf.rs", + line_fragment: "HKDF_extract failed", + reason: "rustls hkdf trait is infallible at this call site", + }, + AllowlistedPanic { + path_suffix: "boring-rustls-provider/src/hkdf.rs", + line_fragment: "HMAC failed", + reason: "rustls hkdf trait is infallible at this call site", + }, + AllowlistedPanic { + path_suffix: "boring-rustls-provider/src/hkdf.rs", + line_fragment: "failed hkdf expand", + reason: "expand_block API is infallible in rustls", + }, + AllowlistedPanic { + path_suffix: "boring-rustls-provider/src/hmac.rs", + line_fragment: "failed getting digest", + reason: "rustls hmac trait is infallible at this call site", + }, + AllowlistedPanic { + path_suffix: "boring-rustls-provider/src/hmac.rs", + line_fragment: "failed initializing hmac", + reason: "rustls hmac trait is infallible at this call site", + }, + AllowlistedPanic { + path_suffix: "boring-rustls-provider/src/hmac.rs", + line_fragment: "failed updating hmac", + reason: "rustls hmac trait is infallible at this call site", + }, + AllowlistedPanic { + path_suffix: "boring-rustls-provider/src/hmac.rs", + line_fragment: "failed hmac final", + reason: "rustls hmac trait is infallible at this call site", + }, + AllowlistedPanic { + path_suffix: "boring-rustls-provider/src/hmac.rs", + line_fragment: "failed creating HMAC_CTX", + reason: "rustls hmac trait is infallible at this call site", + }, + AllowlistedPanic { + path_suffix: "boring-rustls-provider/src/prf.rs", + line_fragment: "failed getting digest", + reason: "rustls tls12::Prf::for_secret is infallible", + }, + AllowlistedPanic { + path_suffix: "boring-rustls-provider/src/prf.rs", + line_fragment: "failed calculating prf", + reason: "rustls tls12::Prf::for_secret is infallible", + }, + AllowlistedPanic { + path_suffix: "boring-rustls-provider/src/verify/rsa.rs", + line_fragment: "BoringRsaVerifier only supports configured RSA schemes", + reason: "static verifier configuration invariant", + }, + AllowlistedPanic { + path_suffix: "boring-rustls-provider/src/verify/ec.rs", + line_fragment: "BoringEcVerifier only supports configured ECDSA schemes", + reason: "static verifier configuration invariant", + }, + AllowlistedPanic { + path_suffix: "boring-rustls-provider/src/verify/ed.rs", + line_fragment: "BoringEdVerifier only supports configured EdDSA schemes", + reason: "static verifier configuration invariant", + }, + AllowlistedPanic { + path_suffix: "boring-rustls-provider/src/kx/ex.rs", + line_fragment: "unsupported key type", + reason: "static KX type invariant", + }, +]; + +#[test] +fn no_unreviewed_runtime_panic_constructs() { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let repo_root = manifest_dir + .parent() + .expect("crate must be within repository root"); + + let mut violations = Vec::new(); + for root in [ + repo_root.join("boring-rustls-provider/src"), + repo_root.join("boring-additions/src"), + ] { + collect_rs_files(&root) + .expect("must be able to enumerate source files") + .into_iter() + .for_each(|path| { + let rel = path + .strip_prefix(repo_root) + .expect("path should be under repo root") + .to_string_lossy() + .to_string(); + + let content = fs::read_to_string(&path) + .unwrap_or_else(|e| panic!("failed to read {}: {e}", rel)); + + for (line_no, line) in runtime_lines_only(&content) { + let trimmed = line.trim(); + if trimmed.starts_with("//") { + continue; + } + + for token in BANNED_TOKENS { + if !line.contains(token) { + continue; + } + + let allowed = ALLOWLIST.iter().find(|allow| { + rel.ends_with(allow.path_suffix) && line.contains(allow.line_fragment) + }); + if allowed.is_none() { + violations.push(format!("{rel}:{line_no}: {trimmed}")); + } + } + } + }); + } + + if !violations.is_empty() { + violations.sort(); + panic!( + "found unreviewed panic constructs in runtime code:\n{}\n\nIf intentional, add a targeted allowlist entry with rationale.", + violations.join("\n") + ); + } +} + +#[test] +fn allowlist_entries_have_matching_runtime_lines() { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let repo_root = manifest_dir + .parent() + .expect("crate must be within repository root"); + + let mut missing = Vec::new(); + for entry in ALLOWLIST { + let path = repo_root.join(entry.path_suffix); + let content = fs::read_to_string(&path) + .unwrap_or_else(|e| panic!("failed to read {}: {e}", entry.path_suffix)); + let found = runtime_lines_only(&content) + .into_iter() + .any(|(_, line)| line.contains(entry.line_fragment)); + if !found { + missing.push(format!( + "{} :: '{}' ({})", + entry.path_suffix, entry.line_fragment, entry.reason + )); + } + } + + if !missing.is_empty() { + missing.sort(); + panic!( + "panic allowlist entries no longer match runtime code:\n{}", + missing.join("\n") + ); + } +} + +fn runtime_lines_only(content: &str) -> Vec<(usize, &str)> { + let mut lines = Vec::new(); + for (index, line) in content.lines().enumerate() { + if line.trim_start().starts_with("#[cfg(test)]") { + break; + } + lines.push((index + 1, line)); + } + lines +} + +fn collect_rs_files(root: &Path) -> Result, std::io::Error> { + let mut files = Vec::new(); + collect_rs_files_rec(root, &mut files)?; + Ok(files) +} + +fn collect_rs_files_rec(root: &Path, acc: &mut Vec) -> Result<(), std::io::Error> { + for entry in fs::read_dir(root)? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + collect_rs_files_rec(&path, acc)?; + continue; + } + + if path.extension().and_then(|ext| ext.to_str()) == Some("rs") { + acc.push(path); + } + } + + Ok(()) +} diff --git a/examples/src/bin/client.rs b/examples/src/bin/client.rs index f1780c1..bbbb32b 100644 --- a/examples/src/bin/client.rs +++ b/examples/src/bin/client.rs @@ -2,7 +2,7 @@ use std::io::{stdout, Read, Write}; use std::net::TcpStream; use std::sync::Arc; -fn main() { +fn main() -> Result<(), Box> { env_logger::init(); let mut root_store = rustls::RootCertStore::empty(); @@ -11,13 +11,15 @@ fn main() { let config = rustls::ClientConfig::builder_with_provider(boring_rustls_provider::provider().into()) .with_safe_default_protocol_versions() - .unwrap() + .map_err(|_| std::io::Error::other("failed selecting protocol versions"))? .with_root_certificates(root_store) .with_no_client_auth(); - let server_name = "www.rust-lang.org".try_into().unwrap(); - let mut conn = rustls::ClientConnection::new(Arc::new(config), server_name).unwrap(); - let mut sock = TcpStream::connect("www.rust-lang.org:443").unwrap(); + let server_name = "www.rust-lang.org" + .try_into() + .map_err(|_| std::io::Error::other("invalid server name"))?; + let mut conn = rustls::ClientConnection::new(Arc::new(config), server_name)?; + let mut sock = TcpStream::connect("www.rust-lang.org:443")?; let mut tls = rustls::Stream::new(&mut conn, &mut sock); tls.write_all( concat!( @@ -28,16 +30,19 @@ fn main() { "\r\n" ) .as_bytes(), - ) - .unwrap(); - let ciphersuite = tls.conn.negotiated_cipher_suite().unwrap(); + )?; + let ciphersuite = tls + .conn + .negotiated_cipher_suite() + .ok_or_else(|| std::io::Error::other("no negotiated ciphersuite"))?; writeln!( &mut std::io::stderr(), "Current ciphersuite: {:?}", ciphersuite.suite() - ) - .unwrap(); + )?; let mut plaintext = Vec::new(); - tls.read_to_end(&mut plaintext).unwrap(); - stdout().write_all(&plaintext).unwrap(); + tls.read_to_end(&mut plaintext)?; + stdout().write_all(&plaintext)?; + + Ok(()) } From b667e1450b9346e0f3ecef0c222d1c37cdc110cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Eng=C3=A9libert?= Date: Mon, 13 Apr 2026 11:44:03 +0200 Subject: [PATCH 10/10] Make algs public --- boring-rustls-provider/src/kx/mod.rs | 2 +- boring-rustls-provider/src/lib.rs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/boring-rustls-provider/src/kx/mod.rs b/boring-rustls-provider/src/kx/mod.rs index 44b524c..c8c0e2b 100644 --- a/boring-rustls-provider/src/kx/mod.rs +++ b/boring-rustls-provider/src/kx/mod.rs @@ -6,7 +6,7 @@ mod ex; #[cfg(feature = "mlkem")] mod pq; #[cfg(feature = "mlkem")] -pub(crate) use pq::X25519MlKem768; +pub use pq::X25519MlKem768; /// Key type discriminant used by [`ex::KeyExchange`] to select the /// appropriate peer key parsing and DH derivation logic. diff --git a/boring-rustls-provider/src/lib.rs b/boring-rustls-provider/src/lib.rs index e02da06..89071cf 100644 --- a/boring-rustls-provider/src/lib.rs +++ b/boring-rustls-provider/src/lib.rs @@ -14,7 +14,7 @@ mod hash; mod helper; mod hkdf; mod hmac; -mod kx; +pub mod kx; #[cfg(feature = "tls12")] mod prf; pub mod sign; @@ -96,7 +96,7 @@ impl rustls::crypto::KeyProvider for Provider { } #[cfg(feature = "fips")] -static ALL_FIPS_CIPHER_SUITES: &[SupportedCipherSuite] = &[ +pub static ALL_FIPS_CIPHER_SUITES: &[SupportedCipherSuite] = &[ SupportedCipherSuite::Tls13(&tls13::AES_256_GCM_SHA384), SupportedCipherSuite::Tls13(&tls13::AES_128_GCM_SHA256), #[cfg(feature = "tls12")] @@ -110,7 +110,7 @@ static ALL_FIPS_CIPHER_SUITES: &[SupportedCipherSuite] = &[ ]; #[cfg(not(feature = "fips"))] -static ALL_CIPHER_SUITES: &[SupportedCipherSuite] = &[ +pub static ALL_CIPHER_SUITES: &[SupportedCipherSuite] = &[ SupportedCipherSuite::Tls13(&tls13::CHACHA20_POLY1305_SHA256), SupportedCipherSuite::Tls13(&tls13::AES_256_GCM_SHA384), SupportedCipherSuite::Tls13(&tls13::AES_128_GCM_SHA256), @@ -134,7 +134,7 @@ static ALL_CIPHER_SUITES: &[SupportedCipherSuite] = &[ /// The `fips` feature implies `mlkem`, so X25519MLKEM768 is always /// available and preferred in FIPS mode. #[cfg(feature = "fips")] -static ALL_FIPS_KX_GROUPS: &[&dyn SupportedKxGroup] = &[ +pub static ALL_FIPS_KX_GROUPS: &[&dyn SupportedKxGroup] = &[ &kx::X25519MlKem768 as _, // PQ hybrid preferred &kx::Secp256r1 as _, // P-256 &kx::Secp384r1 as _, // P-384 @@ -145,7 +145,7 @@ static ALL_FIPS_KX_GROUPS: &[&dyn SupportedKxGroup] = &[ /// Matches boring's default supported group list exactly: /// X25519MLKEM768 (when mlkem enabled), X25519, P-256, P-384. #[cfg(all(not(feature = "fips"), feature = "mlkem"))] -static ALL_KX_GROUPS: &[&dyn SupportedKxGroup] = &[ +pub static ALL_KX_GROUPS: &[&dyn SupportedKxGroup] = &[ &kx::X25519MlKem768 as _, // PQ hybrid preferred &kx::X25519 as _, &kx::Secp256r1 as _, @@ -154,5 +154,5 @@ static ALL_KX_GROUPS: &[&dyn SupportedKxGroup] = &[ /// See [`ALL_KX_GROUPS`] (mlkem variant). #[cfg(not(any(feature = "fips", feature = "mlkem")))] -static ALL_KX_GROUPS: &[&dyn SupportedKxGroup] = +pub static ALL_KX_GROUPS: &[&dyn SupportedKxGroup] = &[&kx::X25519 as _, &kx::Secp256r1 as _, &kx::Secp384r1 as _];