diff --git a/Cargo.toml b/Cargo.toml index a3fc7b4..d94e348 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "boring-rustls-provider" -version = "5.0.0" +version = "6.0.0-dev.0" authors = ["Jan Rüth "] edition = "2024" license = "MIT" @@ -35,7 +35,7 @@ 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", "boring/prf"] +tls12 = ["boring/prf"] # Enable debug logging of BoringSSL errors and provider internals via # the `log` crate. Useful for diagnosing handshake failures. @@ -47,18 +47,16 @@ boring = { version = ">=5.1, <6", default-features = false } boring-sys = { version = ">=5.1, <6", default-features = false } foreign-types = "0.5" log = { version = "0.4.4", optional = true } -rustls = { version = "0.23", default-features = false } +rustls = { version = "0.24.0-dev.0", default-features = false } rustls-pki-types = { version = "1" } -spki = "0.7" +spki = "0.8" zeroize = "1" [dev-dependencies] env_logger = "0.11" hex-literal = "1" -rcgen = "0.12" -rustls = { version = "0.23", features = ["std", "logging"] } -tokio = { version = "1.34", features = ["macros", "rt", "net", "io-util", "io-std"] } -tokio-rustls = { version = "0.26", default-features = false } +rcgen = "0.14" +rustls = { version = "0.24.0-dev.0", features = ["std", "log"] } webpki-roots = { version = "1.0" } [[example]] diff --git a/examples/client.rs b/examples/client.rs index 3378cf3..f89aa89 100644 --- a/examples/client.rs +++ b/examples/client.rs @@ -8,12 +8,10 @@ fn main() -> Result<(), Box> { let mut root_store = rustls::RootCertStore::empty(); root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); - let config = - rustls::ClientConfig::builder_with_provider(boring_rustls_provider::provider().into()) - .with_safe_default_protocol_versions() - .map_err(|_| std::io::Error::other("failed selecting protocol versions"))? - .with_root_certificates(root_store) - .with_no_client_auth(); + let config = rustls::ClientConfig::builder(boring_rustls_provider::provider().into()) + .with_root_certificates(root_store) + .with_no_client_auth() + .map_err(|e| std::io::Error::other(format!("failed building client config: {e}")))?; let server_name = "www.rust-lang.org" .try_into() diff --git a/src/aead.rs b/src/aead.rs index 2395fba..17ada68 100644 --- a/src/aead.rs +++ b/src/aead.rs @@ -3,10 +3,14 @@ use std::marker::PhantomData; use aead::{AeadCore, AeadInPlace, Buffer, Nonce, Tag}; use boring::aead::{AeadCtx, Algorithm}; use boring::error::ErrorStack; +use rustls::ConnectionTrafficSecrets; #[cfg(feature = "tls12")] use rustls::crypto::cipher::make_tls12_aad; -use rustls::crypto::cipher::{self, BorrowedPayload, Iv, PrefixedPayload, make_tls13_aad}; -use rustls::{ConnectionTrafficSecrets, ContentType, ProtocolVersion}; +use rustls::crypto::cipher::{ + self, EncodedMessage, InboundOpaque, Iv, OutboundOpaque, OutboundPlain, make_tls13_aad, +}; +use rustls::enums::{ContentType, ProtocolVersion}; +use rustls_pki_types::FipsStatus; use crate::helper::log_and_map; @@ -90,7 +94,7 @@ impl BoringAeadCrypter { let cipher = ::new_cipher(); - if cipher.nonce_len() != rustls::crypto::cipher::Nonce::new(&iv, 0).0.len() { + if cipher.nonce_len() != rustls::crypto::cipher::Nonce::new(&iv, 0).len() { return Err(ErrorStack::get()); } @@ -141,9 +145,9 @@ where { fn encrypt( &mut self, - msg: cipher::OutboundPlainMessage, + msg: EncodedMessage>, seq: u64, - ) -> Result { + ) -> Result, rustls::Error> { let nonce = cipher::Nonce::new(&self.iv, seq); match self.tls_version { #[cfg(feature = "tls12")] @@ -151,43 +155,40 @@ where let fixed_iv_len = ::FIXED_IV_LEN; let explicit_nonce_len = ::EXPLICIT_NONCE_LEN; - let total_len = self.encrypted_payload_len(msg.payload.len()); + let payload_len = msg.payload.len(); + let total_len = self.encrypted_payload_len(payload_len); - let mut full_payload = PrefixedPayload::with_capacity(total_len); - full_payload.extend_from_slice(&nonce.0.as_ref()[fixed_iv_len..]); + let mut full_payload = OutboundOpaque::with_capacity(total_len); + full_payload.extend_from_slice(&nonce.as_ref()[fixed_iv_len..]); full_payload.extend_from_chunks(&msg.payload); full_payload.extend_from_slice(&vec![0u8; self.max_overhead]); 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()); + let (payload, tag) = payload.split_at_mut(payload_len); + let aad = cipher::make_tls12_aad(seq, msg.typ, msg.version, payload_len); self.ctx - .seal_in_place(&nonce.0, payload, tag, &aad) + .seal_in_place(nonce.as_ref(), payload, tag, &aad) .map_err(|_| rustls::Error::EncryptError)?; - Ok(cipher::OutboundOpaqueMessage::new( - msg.typ, - msg.version, - full_payload, - )) + Ok(EncodedMessage::new(msg.typ, msg.version, full_payload)) } ProtocolVersion::TLSv1_3 => { let total_len = self.encrypted_payload_len(msg.payload.len()); - let mut payload = PrefixedPayload::with_capacity(total_len); + let mut payload = OutboundOpaque::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), + Nonce::::from_slice(nonce.as_ref()), &aad, &mut EncryptBufferAdapter(&mut payload), ) .map_err(|_| rustls::Error::EncryptError) .map(|_| { - cipher::OutboundOpaqueMessage::new( + EncodedMessage::new( ContentType::ApplicationData, ProtocolVersion::TLSv1_2, payload, @@ -217,9 +218,9 @@ where { fn decrypt<'a>( &mut self, - mut m: cipher::InboundOpaqueMessage<'a>, + mut m: EncodedMessage>, seq: u64, - ) -> Result, rustls::Error> { + ) -> Result, rustls::Error> { match self.tls_version { #[cfg(feature = "tls12")] ProtocolVersion::TLSv1_2 => { @@ -251,7 +252,10 @@ where } // grab the IV by constructing a nonce, this is just an xor - let iv = cipher::Nonce::new(&self.iv, seq).0; + let nonce_val = cipher::Nonce::new(&self.iv, seq); + let iv: [u8; 12] = nonce_val + .to_array() + .map_err(|_| rustls::Error::DecryptError)?; let mut nonce = [0u8; 12]; nonce[..fixed_iv_len].copy_from_slice(&iv[..fixed_iv_len]); nonce[fixed_iv_len..].copy_from_slice(explicit_nonce); @@ -278,7 +282,7 @@ where 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), + Nonce::::from_slice(nonce.as_ref()), &aad, &mut DecryptBufferAdapter(&mut m.payload), ) @@ -299,11 +303,16 @@ where packet_number: u64, header: &[u8], payload: &mut [u8], + path_id: Option, ) -> Result { let associated_data = header; - let nonce = cipher::Nonce::new(&self.iv, packet_number); + let nonce = cipher::Nonce::quic(path_id, &self.iv, packet_number); let tag = self - .encrypt_in_place_detached(Nonce::::from_slice(&nonce.0), associated_data, payload) + .encrypt_in_place_detached( + Nonce::::from_slice(nonce.as_ref()), + associated_data, + payload, + ) .map_err(|_| rustls::Error::EncryptError)?; Ok(rustls::quic::Tag::from(tag.as_ref())) @@ -314,9 +323,10 @@ where packet_number: u64, header: &[u8], payload: &'a mut [u8], + path_id: Option, ) -> Result<&'a [u8], rustls::Error> { let associated_data = header; - let nonce = cipher::Nonce::new(&self.iv, packet_number); + let nonce = cipher::Nonce::quic(path_id, &self.iv, packet_number); if payload.len() < self.max_overhead { return Err(rustls::Error::DecryptError); @@ -325,7 +335,7 @@ where let (buffer, tag) = payload.split_at_mut(payload.len() - self.max_overhead); self.ctx - .open_in_place(&nonce.0, buffer, tag, associated_data) + .open_in_place(nonce.as_ref(), buffer, tag, associated_data) .map_err(|_| rustls::Error::DecryptError)?; Ok(buffer) @@ -349,9 +359,9 @@ struct InvalidMessageEncrypter; impl cipher::MessageEncrypter for InvalidMessageEncrypter { fn encrypt( &mut self, - _msg: cipher::OutboundPlainMessage<'_>, + _msg: EncodedMessage>, _seq: u64, - ) -> Result { + ) -> Result, rustls::Error> { Err(rustls::Error::EncryptError) } @@ -365,9 +375,9 @@ struct InvalidMessageDecrypter; impl cipher::MessageDecrypter for InvalidMessageDecrypter { fn decrypt<'a>( &mut self, - _msg: cipher::InboundOpaqueMessage<'a>, + _msg: EncodedMessage>, _seq: u64, - ) -> Result, rustls::Error> { + ) -> Result, rustls::Error> { Err(rustls::Error::DecryptError) } } @@ -380,6 +390,7 @@ impl rustls::quic::PacketKey for InvalidPacketKey< _packet_number: u64, _header: &[u8], _payload: &mut [u8], + _path_id: Option, ) -> Result { Err(rustls::Error::EncryptError) } @@ -389,6 +400,7 @@ impl rustls::quic::PacketKey for InvalidPacketKey< _packet_number: u64, _header: &[u8], _payload: &'a mut [u8], + _path_id: Option, ) -> Result<&'a [u8], rustls::Error> { Err(rustls::Error::DecryptError) } @@ -453,8 +465,12 @@ impl cipher::Tls13AeadAlgorithm for Aead { Ok(::extract_keys(key, iv)) } - fn fips(&self) -> bool { - cfg!(feature = "fips") && ::FIPS_APPROVED + fn fips(&self) -> FipsStatus { + if cfg!(feature = "fips") && ::FIPS_APPROVED { + FipsStatus::Pending + } else { + FipsStatus::Unvalidated + } } } @@ -469,11 +485,14 @@ 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); - match BoringAeadCrypter::::new( - Iv::copy(&full_iv), - key.as_ref(), - ProtocolVersion::TLSv1_2, - ) { + let iv = match Iv::new(&full_iv) { + Ok(iv) => iv, + Err(err) => { + log_and_map("Tls12AeadAlgorithm::encrypter Iv::new", err, ()); + return Box::new(InvalidMessageEncrypter); + } + }; + match BoringAeadCrypter::::new(iv, key.as_ref(), ProtocolVersion::TLSv1_2) { Ok(crypter) => Box::new(crypter), Err(err) => { log_and_map( @@ -490,11 +509,14 @@ impl cipher::Tls12AeadAlgorithm for Aead { 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]); - match BoringAeadCrypter::::new( - Iv::copy(&pseudo_iv), - key.as_ref(), - ProtocolVersion::TLSv1_2, - ) { + let iv = match Iv::new(&pseudo_iv) { + Ok(iv) => iv, + Err(err) => { + log_and_map("Tls12AeadAlgorithm::decrypter Iv::new", err, ()); + return Box::new(InvalidMessageDecrypter); + } + }; + match BoringAeadCrypter::::new(iv, key.as_ref(), ProtocolVersion::TLSv1_2) { Ok(crypter) => Box::new(crypter), Err(err) => { log_and_map( @@ -539,11 +561,16 @@ impl cipher::Tls12AeadAlgorithm for Aead { nonce[fixed_iv_len..].copy_from_slice(explicit); nonce }; - Ok(::extract_keys(key, Iv::copy(&nonce))) + let iv = Iv::new(&nonce).map_err(|_| cipher::UnsupportedOperationError)?; + Ok(::extract_keys(key, iv)) } - fn fips(&self) -> bool { - cfg!(feature = "fips") && ::FIPS_APPROVED + fn fips(&self) -> FipsStatus { + if cfg!(feature = "fips") && ::FIPS_APPROVED { + FipsStatus::Pending + } else { + FipsStatus::Unvalidated + } } } @@ -658,12 +685,16 @@ where ::KEY_SIZE } - fn fips(&self) -> bool { - cfg!(feature = "fips") && ::FIPS_APPROVED + fn fips(&self) -> FipsStatus { + if cfg!(feature = "fips") && ::FIPS_APPROVED { + FipsStatus::Pending + } else { + FipsStatus::Unvalidated + } } } -struct DecryptBufferAdapter<'a, 'p>(&'a mut BorrowedPayload<'p>); +struct DecryptBufferAdapter<'a, 'p>(&'a mut InboundOpaque<'p>); impl AsRef<[u8]> for DecryptBufferAdapter<'_, '_> { fn as_ref(&self) -> &[u8] { @@ -687,7 +718,7 @@ impl Buffer for DecryptBufferAdapter<'_, '_> { } } -struct EncryptBufferAdapter<'a>(&'a mut PrefixedPayload); +struct EncryptBufferAdapter<'a>(&'a mut OutboundOpaque); impl AsRef<[u8]> for EncryptBufferAdapter<'_> { fn as_ref(&self) -> &[u8] { @@ -715,12 +746,11 @@ impl Buffer for EncryptBufferAdapter<'_> { #[cfg(test)] mod tests { use hex_literal::hex; - use rustls::ContentType; - use rustls::ProtocolVersion; use rustls::crypto::cipher::Tls13AeadAlgorithm; - use rustls::crypto::cipher::{AeadKey, InboundOpaqueMessage, Iv}; + use rustls::crypto::cipher::{AeadKey, EncodedMessage, InboundOpaque, Iv}; #[cfg(feature = "tls12")] use rustls::crypto::cipher::{MessageDecrypter, Tls12AeadAlgorithm}; + use rustls::enums::{ContentType, ProtocolVersion}; use rustls::quic::Algorithm as QuicAlgorithm; use rustls::quic::HeaderProtectionKey; @@ -786,10 +816,10 @@ mod tests { Iv::from([0u8; 12]), ); let mut payload = vec![0u8; 8]; - let msg = InboundOpaqueMessage::new( + let msg = EncodedMessage::new( ContentType::ApplicationData, ProtocolVersion::TLSv1_3, - &mut payload, + InboundOpaque(&mut payload), ); let err = decrypter @@ -805,11 +835,11 @@ mod tests { .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); + let enc_err = packet_key.encrypt_in_place(0, &[], &mut payload, None); assert!(matches!(enc_err, Err(rustls::Error::EncryptError))); let dec_err = packet_key - .decrypt_in_place(0, &[], &mut payload) + .decrypt_in_place(0, &[], &mut payload, None) .expect_err("invalid constructor inputs should produce decrypt errors"); assert!(matches!(dec_err, rustls::Error::DecryptError)); } @@ -823,10 +853,10 @@ mod tests { &[0u8; 4], ); let mut payload = vec![0u8; 8]; - let msg = InboundOpaqueMessage::new( + let msg = EncodedMessage::new( ContentType::ApplicationData, ProtocolVersion::TLSv1_2, - &mut payload, + InboundOpaque(&mut payload), ); let err = decrypter @@ -857,14 +887,14 @@ mod tests { let mut payload = expected_cleartext; let tag = protector - .encrypt_in_place(packet_number, &unprotected_header, &mut payload) + .encrypt_in_place(packet_number, &unprotected_header, &mut payload, None) .unwrap(); let mut ciphertext = [&payload, tag.as_ref()].concat(); assert_eq!(ciphertext, expected_ciphertext); let cleartext = protector - .decrypt_in_place(packet_number, &unprotected_header, &mut ciphertext) + .decrypt_in_place(packet_number, &unprotected_header, &mut ciphertext, None) .unwrap(); assert_eq!(cleartext, expected_cleartext); @@ -883,7 +913,7 @@ mod tests { let mut payload = [0u8; ::TAG_LEN - 1]; let err = crypter - .decrypt_in_place(0, &[], &mut payload) + .decrypt_in_place(0, &[], &mut payload, None) .expect_err("truncated QUIC payload must fail decryption"); assert!(matches!(err, rustls::Error::DecryptError)); @@ -900,10 +930,10 @@ mod tests { let min_payload_len = ::EXPLICIT_NONCE_LEN + ::TAG_LEN; let mut payload = vec![0u8; min_payload_len - 1]; - let msg = InboundOpaqueMessage::new( + let msg = EncodedMessage::new( ContentType::ApplicationData, ProtocolVersion::TLSv1_2, - &mut payload, + InboundOpaque(&mut payload), ); let err = decrypter diff --git a/src/hash.rs b/src/hash.rs index 95ddc41..6a96ac0 100644 --- a/src/hash.rs +++ b/src/hash.rs @@ -1,5 +1,6 @@ use boring::hash::{Hasher, MessageDigest}; use rustls::crypto::hash; +use rustls_pki_types::FipsStatus; pub const SHA256: &dyn hash::Hash = &Hash(boring::nid::Nid::SHA256); pub const SHA384: &dyn hash::Hash = &Hash(boring::nid::Nid::SHA384); @@ -19,17 +20,21 @@ impl hash::Hash for Hash { hasher.finish() } - fn algorithm(&self) -> hash::HashAlgorithm { + fn algorithm(&self) -> rustls::crypto::HashAlgorithm { match self.0 { - boring::nid::Nid::SHA256 => hash::HashAlgorithm::SHA256, - boring::nid::Nid::SHA384 => hash::HashAlgorithm::SHA384, - boring::nid::Nid::SHA512 => hash::HashAlgorithm::SHA512, + boring::nid::Nid::SHA256 => rustls::crypto::HashAlgorithm::SHA256, + boring::nid::Nid::SHA384 => rustls::crypto::HashAlgorithm::SHA384, + boring::nid::Nid::SHA512 => rustls::crypto::HashAlgorithm::SHA512, _ => unreachable!("hash::Hash is only instantiated with SHA-2 digests"), } } - fn fips(&self) -> bool { - cfg!(feature = "fips") + fn fips(&self) -> FipsStatus { + if cfg!(feature = "fips") { + FipsStatus::Pending + } else { + FipsStatus::Unvalidated + } } fn output_len(&self) -> usize { diff --git a/src/hkdf.rs b/src/hkdf.rs index d4b070d..fad2a6a 100644 --- a/src/hkdf.rs +++ b/src/hkdf.rs @@ -139,8 +139,12 @@ impl RustlsHkdf for Hkdf { rustls::crypto::hmac::Tag::new(&hash[..hash_len as usize]) } - fn fips(&self) -> bool { - cfg!(feature = "fips") + fn fips(&self) -> rustls_pki_types::FipsStatus { + if cfg!(feature = "fips") { + rustls_pki_types::FipsStatus::Pending + } else { + rustls_pki_types::FipsStatus::Unvalidated + } } } diff --git a/src/hmac.rs b/src/hmac.rs index 7dc0573..ac3c834 100644 --- a/src/hmac.rs +++ b/src/hmac.rs @@ -28,8 +28,12 @@ impl crypto::hmac::Hmac for BoringHmac { .size() } - fn fips(&self) -> bool { - cfg!(feature = "fips") + fn fips(&self) -> rustls_pki_types::FipsStatus { + if cfg!(feature = "fips") { + rustls_pki_types::FipsStatus::Pending + } else { + rustls_pki_types::FipsStatus::Unvalidated + } } } diff --git a/src/kx/ex.rs b/src/kx/ex.rs index 11482bc..dbb273f 100644 --- a/src/kx/ex.rs +++ b/src/kx/ex.rs @@ -5,7 +5,8 @@ use boring::{ pkey::Id, pkey::{PKey, PKeyRef, Private}, }; -use rustls::crypto; +use rustls::crypto::kx::{ActiveKeyExchange, NamedGroup, SharedSecret}; +use rustls::error::PeerMisbehaved; use crate::helper::log_and_map; @@ -114,18 +115,15 @@ impl KeyExchange { } } -impl crypto::ActiveKeyExchange for KeyExchange { - fn complete( - self: Box, - peer_pub_key: &[u8], - ) -> Result { +impl ActiveKeyExchange for KeyExchange { + fn complete(self: Box, peer_pub_key: &[u8]) -> Result { self.diffie_hellman(peer_pub_key) - .map(crypto::SharedSecret::from) + .map(SharedSecret::from) .map_err(|e| { log_and_map( "ex::KeyExchange::diffie_hellman", e, - rustls::Error::PeerMisbehaved(rustls::PeerMisbehaved::InvalidKeyShare), + rustls::Error::PeerMisbehaved(PeerMisbehaved::InvalidKeyShare), ) }) } @@ -134,12 +132,12 @@ impl crypto::ActiveKeyExchange for KeyExchange { &self.pub_bytes } - fn group(&self) -> rustls::NamedGroup { + fn group(&self) -> NamedGroup { match self.key_type { #[cfg(not(feature = "fips"))] - DhKeyType::ED(boring_sys::NID_X25519) => rustls::NamedGroup::X25519, - DhKeyType::EC((_, boring_sys::NID_X9_62_prime256v1)) => rustls::NamedGroup::secp256r1, - DhKeyType::EC((_, boring_sys::NID_secp384r1)) => rustls::NamedGroup::secp384r1, + DhKeyType::ED(boring_sys::NID_X25519) => NamedGroup::X25519, + DhKeyType::EC((_, boring_sys::NID_X9_62_prime256v1)) => NamedGroup::secp256r1, + DhKeyType::EC((_, boring_sys::NID_secp384r1)) => NamedGroup::secp384r1, _ => unreachable!("unsupported key type"), } } @@ -148,7 +146,7 @@ impl crypto::ActiveKeyExchange for KeyExchange { #[cfg(test)] mod tests { use super::KeyExchange; - use rustls::crypto::ActiveKeyExchange; + use rustls::crypto::kx::ActiveKeyExchange; #[test] fn test_derive_ec() { diff --git a/src/kx/mod.rs b/src/kx/mod.rs index 44b524c..1de9f42 100644 --- a/src/kx/mod.rs +++ b/src/kx/mod.rs @@ -1,4 +1,8 @@ -use rustls::crypto::{self, ActiveKeyExchange}; +use rustls::crypto::{ + self, + kx::{NamedGroup, StartedKeyExchange, SupportedKxGroup}, +}; +use rustls_pki_types::FipsStatus; use crate::helper::log_and_map; @@ -22,19 +26,20 @@ enum DhKeyType { 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| { - log_and_map("X25519.start", e, crypto::GetRandomFailed) - })?)) +impl SupportedKxGroup for X25519 { + fn start(&self) -> Result { + Ok(StartedKeyExchange::Single(Box::new( + ex::KeyExchange::with_x25519() + .map_err(|e| log_and_map("X25519.start", e, crypto::GetRandomFailed))?, + ))) } - fn name(&self) -> rustls::NamedGroup { - rustls::NamedGroup::X25519 + fn name(&self) -> NamedGroup { + NamedGroup::X25519 } - fn fips(&self) -> bool { - false + fn fips(&self) -> FipsStatus { + FipsStatus::Unvalidated } } @@ -42,19 +47,24 @@ impl crypto::SupportedKxGroup for X25519 { #[derive(Debug)] pub struct Secp256r1; -impl crypto::SupportedKxGroup for Secp256r1 { - fn start(&self) -> Result, rustls::Error> { - Ok(Box::new(ex::KeyExchange::with_secp256r1().map_err( - |e| log_and_map("Secp256r1.start", e, crypto::GetRandomFailed), - )?)) +impl SupportedKxGroup for Secp256r1 { + fn start(&self) -> Result { + Ok(StartedKeyExchange::Single(Box::new( + ex::KeyExchange::with_secp256r1() + .map_err(|e| log_and_map("Secp256r1.start", e, crypto::GetRandomFailed))?, + ))) } - fn name(&self) -> rustls::NamedGroup { - rustls::NamedGroup::secp256r1 + fn name(&self) -> NamedGroup { + NamedGroup::secp256r1 } - fn fips(&self) -> bool { - cfg!(feature = "fips") + fn fips(&self) -> FipsStatus { + if cfg!(feature = "fips") { + FipsStatus::Pending + } else { + FipsStatus::Unvalidated + } } } @@ -62,18 +72,23 @@ impl crypto::SupportedKxGroup for Secp256r1 { #[derive(Debug)] pub struct Secp384r1; -impl crypto::SupportedKxGroup for Secp384r1 { - fn start(&self) -> Result, rustls::Error> { - Ok(Box::new(ex::KeyExchange::with_secp384r1().map_err( - |e| log_and_map("Secp384r1.start", e, crypto::GetRandomFailed), - )?)) +impl SupportedKxGroup for Secp384r1 { + fn start(&self) -> Result { + Ok(StartedKeyExchange::Single(Box::new( + ex::KeyExchange::with_secp384r1() + .map_err(|e| log_and_map("Secp384r1.start", e, crypto::GetRandomFailed))?, + ))) } - fn name(&self) -> rustls::NamedGroup { - rustls::NamedGroup::secp384r1 + fn name(&self) -> NamedGroup { + NamedGroup::secp384r1 } - fn fips(&self) -> bool { - cfg!(feature = "fips") + fn fips(&self) -> FipsStatus { + if cfg!(feature = "fips") { + FipsStatus::Pending + } else { + FipsStatus::Unvalidated + } } } diff --git a/src/kx/pq.rs b/src/kx/pq.rs index f5c91fe..501f63c 100644 --- a/src/kx/pq.rs +++ b/src/kx/pq.rs @@ -10,8 +10,16 @@ //! - 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 rustls::Error; +use rustls::crypto::{ + self, + kx::{ + ActiveKeyExchange, CompletedKeyExchange, HybridKeyExchange, NamedGroup, SharedSecret, + StartedKeyExchange, SupportedKxGroup, + }, +}; +use rustls::error::PeerMisbehaved; +use rustls_pki_types::FipsStatus; use zeroize::Zeroizing; const MLKEM768_PUBLIC_KEY_BYTES: usize = 1184; @@ -27,9 +35,9 @@ const SERVER_SHARE_LEN: usize = MLKEM768_CIPHERTEXT_BYTES + X25519_PUBLIC_KEY_BY #[derive(Debug)] pub struct X25519MlKem768; -impl crypto::SupportedKxGroup for X25519MlKem768 { +impl SupportedKxGroup for X25519MlKem768 { /// Client-side: generate ML-KEM-768 + X25519 keypairs. - fn start(&self) -> Result, Error> { + fn start(&self) -> Result { let (mlkem_pub, mlkem_priv) = MlKemPrivateKey::generate(Algorithm::MlKem768).map_err(|e| { crate::helper::log_and_map( @@ -51,12 +59,12 @@ impl crypto::SupportedKxGroup for X25519MlKem768 { pub_key.extend_from_slice(mlkem_pub.as_bytes()); pub_key.extend_from_slice(&x25519_pub); - Ok(Box::new(ActiveX25519MlKem768 { + Ok(StartedKeyExchange::Hybrid(Box::new(ActiveX25519MlKem768 { mlkem_priv, x25519_priv, x25519_pub, pub_key, - })) + }))) } /// Server-side: one-shot encapsulate + DH. @@ -65,9 +73,7 @@ impl crypto::SupportedKxGroup for X25519MlKem768 { /// 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, - )); + return Err(Error::PeerMisbehaved(PeerMisbehaved::InvalidKeyShare)); } // Split client share: mlkem_pk(1184) || x25519_pk(32) @@ -81,7 +87,7 @@ impl crypto::SupportedKxGroup for X25519MlKem768 { crate::helper::log_and_map( "X25519MlKem768::start_and_complete mlkem parse", e, - Error::PeerMisbehaved(rustls::PeerMisbehaved::InvalidKeyShare), + Error::PeerMisbehaved(PeerMisbehaved::InvalidKeyShare), ) }, )?; @@ -90,7 +96,7 @@ impl crypto::SupportedKxGroup for X25519MlKem768 { crate::helper::log_and_map( "X25519MlKem768::start_and_complete mlkem encap", e, - Error::PeerMisbehaved(rustls::PeerMisbehaved::InvalidKeyShare), + Error::PeerMisbehaved(PeerMisbehaved::InvalidKeyShare), ) })?; let mlkem_ss = Zeroizing::new(mlkem_ss); @@ -113,9 +119,7 @@ impl crypto::SupportedKxGroup for X25519MlKem768 { client_x25519_pk.as_ptr(), ); if rc != 1 { - return Err(Error::PeerMisbehaved( - rustls::PeerMisbehaved::InvalidKeyShare, - )); + return Err(Error::PeerMisbehaved(PeerMisbehaved::InvalidKeyShare)); } } @@ -140,12 +144,12 @@ impl crypto::SupportedKxGroup for X25519MlKem768 { NamedGroup::X25519MLKEM768 } - fn fips(&self) -> bool { - cfg!(feature = "fips") - } - - fn usable_for_version(&self, version: ProtocolVersion) -> bool { - version == ProtocolVersion::TLSv1_3 + fn fips(&self) -> FipsStatus { + if cfg!(feature = "fips") { + FipsStatus::Pending + } else { + FipsStatus::Unvalidated + } } } @@ -160,13 +164,47 @@ struct ActiveX25519MlKem768 { pub_key: Vec, } -impl crypto::ActiveKeyExchange for ActiveX25519MlKem768 { +impl HybridKeyExchange for ActiveX25519MlKem768 { + fn component(&self) -> (NamedGroup, &[u8]) { + (NamedGroup::X25519, &self.x25519_pub) + } + + fn complete_component(self: Box, peer_pub_key: &[u8]) -> Result { + if peer_pub_key.len() != X25519_PUBLIC_KEY_BYTES { + return Err(Error::PeerMisbehaved(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(PeerMisbehaved::InvalidKeyShare)); + } + } + + Ok(SharedSecret::from(Vec::from(&x25519_ss[..]))) + } + + fn as_key_exchange(&self) -> &(dyn ActiveKeyExchange + 'static) { + self + } + + fn into_key_exchange(self: Box) -> Box { + self + } +} + +impl 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, - )); + return Err(Error::PeerMisbehaved(PeerMisbehaved::InvalidKeyShare)); } // Split server share: mlkem_ct(1088) || x25519_pk(32) @@ -177,7 +215,7 @@ impl crypto::ActiveKeyExchange for ActiveX25519MlKem768 { crate::helper::log_and_map( "ActiveX25519MlKem768::complete mlkem decap", e, - Error::PeerMisbehaved(rustls::PeerMisbehaved::InvalidKeyShare), + Error::PeerMisbehaved(PeerMisbehaved::InvalidKeyShare), ) })?; let mlkem_ss = Zeroizing::new(mlkem_ss); @@ -192,9 +230,7 @@ impl crypto::ActiveKeyExchange for ActiveX25519MlKem768 { server_x25519_pk.as_ptr(), ); if rc != 1 { - return Err(Error::PeerMisbehaved( - rustls::PeerMisbehaved::InvalidKeyShare, - )); + return Err(Error::PeerMisbehaved(PeerMisbehaved::InvalidKeyShare)); } } @@ -206,39 +242,6 @@ impl crypto::ActiveKeyExchange for ActiveX25519MlKem768 { 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 } @@ -251,14 +254,21 @@ impl crypto::ActiveKeyExchange for ActiveX25519MlKem768 { #[cfg(test)] mod tests { use super::*; - use rustls::crypto::SupportedKxGroup; + use rustls::crypto::kx::SupportedKxGroup; + + fn unwrap_hybrid(started: StartedKeyExchange) -> Box { + match started { + StartedKeyExchange::Hybrid(h) => h, + _ => panic!("expected Hybrid variant"), + } + } #[test] fn hybrid_round_trip() { let group = X25519MlKem768; // Client generates keypair - let client = group.start().unwrap(); + let client = unwrap_hybrid(group.start().unwrap()); assert_eq!(client.pub_key().len(), CLIENT_SHARE_LEN); assert_eq!(client.group(), NamedGroup::X25519MLKEM768); @@ -268,7 +278,10 @@ mod tests { assert_eq!(server.group, NamedGroup::X25519MLKEM768); // Client decapsulates + derives - let client_secret = client.complete(&server.pub_key).unwrap(); + let client_secret = client + .into_key_exchange() + .complete(&server.pub_key) + .unwrap(); // Shared secrets must match assert_eq!( @@ -289,19 +302,17 @@ mod tests { #[test] fn rejects_invalid_server_share() { let group = X25519MlKem768; - let client = group.start().unwrap(); - let result = client.complete(&[0u8; 100]); + let client = unwrap_hybrid(group.start().unwrap()); + let result = client.into_key_exchange().complete(&[0u8; 100]); assert!(result.is_err()); } #[test] fn exposes_x25519_hybrid_component() { let group = X25519MlKem768; - let client = group.start().unwrap(); + let client = unwrap_hybrid(group.start().unwrap()); - let (component_group, component_pub_key) = client - .hybrid_component() - .expect("hybrid component should be available"); + let (component_group, component_pub_key) = client.component(); assert_eq!(component_group, NamedGroup::X25519); assert_eq!(component_pub_key.len(), X25519_PUBLIC_KEY_BYTES); @@ -314,10 +325,9 @@ mod tests { #[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 client = unwrap_hybrid(group.start().unwrap()); + let (_, client_x25519_pub) = client.component(); + let client_x25519_pub = client_x25519_pub.to_vec(); let mut server_x25519_pub = [0u8; X25519_PUBLIC_KEY_BYTES]; let mut server_x25519_priv = [0u8; X25519_PRIVATE_KEY_BYTES]; @@ -337,28 +347,19 @@ mod tests { assert_eq!(rc, 1); } - let client_secret = client - .complete_hybrid_component(&server_x25519_pub) - .unwrap(); + let client_secret = client.complete_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()); + assert_eq!(X25519MlKem768.fips(), FipsStatus::Pending); } #[test] #[cfg(not(feature = "fips"))] fn reports_non_fips() { - assert!(!X25519MlKem768.fips()); + assert_eq!(X25519MlKem768.fips(), FipsStatus::Unvalidated); } } diff --git a/src/lib.rs b/src/lib.rs index 41e635c..756bbc7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,13 +3,12 @@ //! # Quick start //! //! ```no_run -//! let config = rustls::ClientConfig::builder_with_provider( +//! let config = rustls::ClientConfig::builder( //! boring_rustls_provider::provider().into(), //! ) -//! .with_safe_default_protocol_versions() -//! .unwrap() //! .with_root_certificates(rustls::RootCertStore::empty()) -//! .with_no_client_auth(); +//! .with_no_client_auth() +//! .unwrap(); //! ``` //! //! # Features @@ -25,16 +24,16 @@ //! - **`logging`** — Enable debug logging via the [`log`](https://docs.rs/log) //! crate. -use std::sync::Arc; +use std::borrow::Cow; use helper::log_and_map; #[cfg(all(feature = "fips", feature = "log"))] use log::warn; use rustls::{ SupportedCipherSuite, - crypto::{CryptoProvider, GetRandomFailed, SupportedKxGroup}, + crypto::{CryptoProvider, GetRandomFailed, kx::SupportedKxGroup}, }; -use rustls_pki_types::PrivateKeyDer; +use rustls_pki_types::{FipsStatus, PrivateKeyDer}; mod aead; mod hash; @@ -81,7 +80,7 @@ pub fn provider() -> CryptoProvider { /// /// When the `fips` feature is enabled, any non-FIPS cipher suites in /// `ciphers` are silently filtered out. -pub fn provider_with_ciphers(ciphers: Vec) -> CryptoProvider { +pub fn provider_with_ciphers(ciphers: Vec) -> CryptoProvider { #[cfg(feature = "fips")] let ciphers = { let original_len = ciphers.len(); @@ -101,18 +100,33 @@ pub fn provider_with_ciphers(ciphers: Vec) -> Cryp filtered }; + // Split cipher suites by TLS version + #[cfg_attr(not(feature = "tls12"), allow(unused_mut))] + let mut tls12_suites = Vec::new(); + let mut tls13_suites = Vec::new(); + for suite in ciphers { + match suite { + SupportedCipherSuite::Tls13(s) => tls13_suites.push(s), + #[cfg(feature = "tls12")] + SupportedCipherSuite::Tls12(s) => tls12_suites.push(s), + _ => {} + } + } + CryptoProvider { - cipher_suites: ciphers, + tls13_cipher_suites: Cow::Owned(tls13_suites), + tls12_cipher_suites: Cow::Owned(tls12_suites), #[cfg(feature = "fips")] - kx_groups: ALL_FIPS_KX_GROUPS.to_vec(), + kx_groups: Cow::Borrowed(ALL_FIPS_KX_GROUPS), #[cfg(not(feature = "fips"))] - kx_groups: ALL_KX_GROUPS.to_vec(), + kx_groups: Cow::Borrowed(ALL_KX_GROUPS), #[cfg(feature = "fips")] signature_verification_algorithms: verify::ALL_FIPS_ALGORITHMS, #[cfg(not(feature = "fips"))] signature_verification_algorithms: verify::ALL_ALGORITHMS, secure_random: &Provider, key_provider: &Provider, + ticketer_factory: &Provider, } } @@ -124,8 +138,12 @@ impl rustls::crypto::SecureRandom for Provider { boring::rand::rand_bytes(bytes).map_err(|e| log_and_map("rand_bytes", e, GetRandomFailed)) } - fn fips(&self) -> bool { - cfg!(feature = "fips") + fn fips(&self) -> FipsStatus { + if cfg!(feature = "fips") { + FipsStatus::Pending + } else { + FipsStatus::Unvalidated + } } } @@ -133,12 +151,32 @@ impl rustls::crypto::KeyProvider for Provider { fn load_private_key( &self, key_der: PrivateKeyDer<'static>, - ) -> Result, rustls::Error> { - sign::BoringPrivateKey::try_from(key_der).map(|x| Arc::new(x) as _) + ) -> Result, rustls::Error> { + sign::BoringPrivateKey::try_from(key_der).map(|x| Box::new(x) as _) + } + + fn fips(&self) -> FipsStatus { + if cfg!(feature = "fips") { + FipsStatus::Pending + } else { + FipsStatus::Unvalidated + } + } +} + +impl rustls::crypto::TicketerFactory for Provider { + fn ticketer( + &self, + ) -> Result, rustls::Error> { + Err(rustls::Error::General("tickets not supported".into())) } - fn fips(&self) -> bool { - cfg!(feature = "fips") + fn fips(&self) -> FipsStatus { + if cfg!(feature = "fips") { + FipsStatus::Pending + } else { + FipsStatus::Unvalidated + } } } diff --git a/src/prf.rs b/src/prf.rs index 3869d35..47bd8de 100644 --- a/src/prf.rs +++ b/src/prf.rs @@ -1,5 +1,6 @@ use boring::error::ErrorStack; use rustls::crypto; +use rustls_pki_types::FipsStatus; use crate::helper::log_and_map; @@ -9,7 +10,7 @@ impl crypto::tls12::Prf for PrfTls1WithDigest { fn for_key_exchange( &self, output: &mut [u8; 48], - kx: Box, + kx: Box, peer_pub_key: &[u8], label: &[u8], seed: &[u8], @@ -23,13 +24,32 @@ impl crypto::tls12::Prf for PrfTls1WithDigest { .map_err(|e| log_and_map("prf", e, rustls::Error::General("failed on prf".into()))) } - fn for_secret(&self, output: &mut [u8], secret: &[u8], label: &[u8], seed: &[u8]) { - let digest = boring::hash::MessageDigest::from_nid(self.0).expect("failed getting digest"); - prf(digest, output, secret, label, seed).expect("failed calculating prf") + fn new_secret(&self, master_secret: &[u8; 48]) -> Box { + Box::new(PrfSecret { + nid: self.0, + master_secret: *master_secret, + }) } - fn fips(&self) -> bool { - cfg!(feature = "fips") + fn fips(&self) -> FipsStatus { + if cfg!(feature = "fips") { + FipsStatus::Pending + } else { + FipsStatus::Unvalidated + } + } +} + +struct PrfSecret { + nid: boring::nid::Nid, + master_secret: [u8; 48], +} + +impl crypto::tls12::PrfSecret for PrfSecret { + fn prf(&self, output: &mut [u8], label: &[u8], seed: &[u8]) { + let digest = + boring::hash::MessageDigest::from_nid(self.nid).expect("failed getting digest"); + prf(digest, output, &self.master_secret, label, seed).expect("failed calculating prf") } } diff --git a/src/sign.rs b/src/sign.rs index 4cb85f1..3f74430 100644 --- a/src/sign.rs +++ b/src/sign.rs @@ -7,7 +7,7 @@ use boring::{ rsa::Padding, sign::{RsaPssSaltlen, Signer}, }; -use rustls::{SignatureScheme, sign::SigningKey}; +use rustls::crypto::{SignatureScheme, SigningKey}; use rustls_pki_types::PrivateKeyDer; use crate::helper::log_and_map; @@ -190,8 +190,8 @@ fn ec_signer_from_params( impl SigningKey for BoringPrivateKey { fn choose_scheme( &self, - offered: &[rustls::SignatureScheme], - ) -> Option> { + offered: &[SignatureScheme], + ) -> Option> { let scheme = match self.1 { KeyKind::Rsa => ALL_RSA_SCHEMES .iter() @@ -205,11 +205,11 @@ impl SigningKey for BoringPrivateKey { 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) + KeyKind::Ed25519 if offered.contains(&SignatureScheme::ED25519) => { + Some(&SignatureScheme::ED25519) } - KeyKind::Ed448 if offered.contains(&rustls::SignatureScheme::ED448) => { - Some(&rustls::SignatureScheme::ED448) + KeyKind::Ed448 if offered.contains(&SignatureScheme::ED448) => { + Some(&SignatureScheme::ED448) } _ => None, }?; @@ -217,19 +217,15 @@ impl SigningKey for BoringPrivateKey { Some(Box::new(BoringSigner(self.0.clone(), *scheme))) } - fn algorithm(&self) -> rustls::SignatureAlgorithm { - match self.1 { - KeyKind::Rsa => rustls::SignatureAlgorithm::RSA, - KeyKind::Ec(_) => rustls::SignatureAlgorithm::ECDSA, - KeyKind::Ed25519 => rustls::SignatureAlgorithm::ED25519, - KeyKind::Ed448 => rustls::SignatureAlgorithm::ED448, - } + fn public_key(&self) -> Option> { + let spki_der = self.0.public_key_to_der().ok()?; + Some(rustls_pki_types::SubjectPublicKeyInfoDer::from(spki_der)) } } /// A boringssl-based Signer. #[derive(Debug)] -pub struct BoringSigner(Arc>, rustls::SignatureScheme); +pub struct BoringSigner(Arc>, SignatureScheme); impl BoringSigner { fn get_signer(&self) -> Result, rustls::Error> { @@ -277,8 +273,8 @@ impl BoringSigner { } } -impl rustls::sign::Signer for BoringSigner { - fn sign(&self, message: &[u8]) -> Result, rustls::Error> { +impl rustls::crypto::Signer for BoringSigner { + fn sign(self: Box, message: &[u8]) -> Result, rustls::Error> { let mut signer = self.get_signer()?; let max_sig_len = signer .len() @@ -296,7 +292,7 @@ impl rustls::sign::Signer for BoringSigner { Ok(sig) } - fn scheme(&self) -> rustls::SignatureScheme { + fn scheme(&self) -> SignatureScheme { self.1 } } @@ -309,8 +305,7 @@ mod tests { pkey::{PKey, Private}, rsa::Rsa, }; - use rustls::sign::SigningKey; - use rustls::{SignatureAlgorithm, SignatureScheme}; + use rustls::crypto::{SignatureScheme, SigningKey}; use rustls_pki_types::{PrivateKeyDer, PrivatePkcs8KeyDer, PrivateSec1KeyDer}; use super::BoringPrivateKey; @@ -329,7 +324,7 @@ mod tests { let key = BoringPrivateKey::try_from(key_der).expect("SEC1 private key should load"); - assert_eq!(key.algorithm(), SignatureAlgorithm::ECDSA); + assert!(key.public_key().is_some()); } #[test] diff --git a/src/tls12.rs b/src/tls12.rs index 39211c3..3488385 100644 --- a/src/tls12.rs +++ b/src/tls12.rs @@ -1,4 +1,5 @@ -use rustls::{SignatureScheme, Tls12CipherSuite, crypto}; +use rustls::Tls12CipherSuite; +use rustls::crypto::{self, CipherSuite, SignatureScheme}; use crate::{aead, hash, prf}; @@ -24,53 +25,57 @@ const PRF_SHA384: prf::PrfTls1WithDigest = prf::PrfTls1WithDigest(boring::nid::N /// TLS 1.2 ECDHE-ECDSA with AES-128-GCM and SHA-256. pub static ECDHE_ECDSA_AES128_GCM_SHA256: Tls12CipherSuite = Tls12CipherSuite { + protocol_version: rustls::version::TLS12_VERSION, common: rustls::crypto::CipherSuiteCommon { - suite: rustls::CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + suite: CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, hash_provider: hash::SHA256, confidentiality_limit: 1 << 23, }, aead_alg: &aead::Aead::::DEFAULT, prf_provider: &PRF_SHA256, - kx: crypto::KeyExchangeAlgorithm::ECDHE, + kx: crypto::kx::KeyExchangeAlgorithm::ECDHE, sign: ALL_ECDSA_SCHEMES, }; /// TLS 1.2 ECDHE-RSA with AES-128-GCM and SHA-256. pub static ECDHE_RSA_AES128_GCM_SHA256: Tls12CipherSuite = Tls12CipherSuite { + protocol_version: rustls::version::TLS12_VERSION, common: rustls::crypto::CipherSuiteCommon { - suite: rustls::CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + suite: CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, hash_provider: hash::SHA256, confidentiality_limit: 1 << 23, }, aead_alg: &aead::Aead::::DEFAULT, prf_provider: &PRF_SHA256, - kx: crypto::KeyExchangeAlgorithm::ECDHE, + kx: crypto::kx::KeyExchangeAlgorithm::ECDHE, sign: ALL_RSA_SCHEMES, }; /// TLS 1.2 ECDHE-ECDSA with AES-256-GCM and SHA-384. pub static ECDHE_ECDSA_AES256_GCM_SHA384: Tls12CipherSuite = Tls12CipherSuite { + protocol_version: rustls::version::TLS12_VERSION, common: rustls::crypto::CipherSuiteCommon { - suite: rustls::CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + suite: CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, hash_provider: hash::SHA384, confidentiality_limit: 1 << 23, }, aead_alg: &aead::Aead::::DEFAULT, prf_provider: &PRF_SHA384, - kx: crypto::KeyExchangeAlgorithm::ECDHE, + kx: crypto::kx::KeyExchangeAlgorithm::ECDHE, sign: ALL_ECDSA_SCHEMES, }; /// TLS 1.2 ECDHE-RSA with AES-256-GCM and SHA-384. pub static ECDHE_RSA_AES256_GCM_SHA384: Tls12CipherSuite = Tls12CipherSuite { + protocol_version: rustls::version::TLS12_VERSION, common: rustls::crypto::CipherSuiteCommon { - suite: rustls::CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + suite: CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, hash_provider: hash::SHA384, confidentiality_limit: 1 << 23, }, aead_alg: &aead::Aead::::DEFAULT, prf_provider: &PRF_SHA384, - kx: crypto::KeyExchangeAlgorithm::ECDHE, + kx: crypto::kx::KeyExchangeAlgorithm::ECDHE, sign: ALL_RSA_SCHEMES, }; @@ -78,14 +83,15 @@ pub static ECDHE_RSA_AES256_GCM_SHA384: Tls12CipherSuite = Tls12CipherSuite { /// /// Not available in FIPS mode. pub static ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256: Tls12CipherSuite = Tls12CipherSuite { + protocol_version: rustls::version::TLS12_VERSION, common: rustls::crypto::CipherSuiteCommon { - suite: rustls::CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + suite: CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, hash_provider: hash::SHA256, confidentiality_limit: u64::MAX, }, aead_alg: &aead::Aead::::DEFAULT, prf_provider: &PRF_SHA256, - kx: crypto::KeyExchangeAlgorithm::ECDHE, + kx: crypto::kx::KeyExchangeAlgorithm::ECDHE, sign: ALL_ECDSA_SCHEMES, }; @@ -93,13 +99,14 @@ pub static ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256: Tls12CipherSuite = Tls12Ci /// /// Not available in FIPS mode. pub static ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256: Tls12CipherSuite = Tls12CipherSuite { + protocol_version: rustls::version::TLS12_VERSION, common: rustls::crypto::CipherSuiteCommon { - suite: rustls::CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + suite: CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, hash_provider: hash::SHA256, confidentiality_limit: u64::MAX, }, aead_alg: &aead::Aead::::DEFAULT, prf_provider: &PRF_SHA256, - kx: crypto::KeyExchangeAlgorithm::ECDHE, + kx: crypto::kx::KeyExchangeAlgorithm::ECDHE, sign: ALL_RSA_SCHEMES, }; diff --git a/src/tls13.rs b/src/tls13.rs index 30a06e3..90cd454 100644 --- a/src/tls13.rs +++ b/src/tls13.rs @@ -4,8 +4,9 @@ use crate::{aead, hash, hkdf}; /// TLS 1.3 AES-128-GCM with SHA-256. pub static AES_128_GCM_SHA256: Tls13CipherSuite = Tls13CipherSuite { + protocol_version: rustls::version::TLS13_VERSION, common: rustls::crypto::CipherSuiteCommon { - suite: rustls::CipherSuite::TLS13_AES_128_GCM_SHA256, + suite: rustls::crypto::CipherSuite::TLS13_AES_128_GCM_SHA256, hash_provider: hash::SHA256, confidentiality_limit: 1 << 23, }, @@ -16,8 +17,9 @@ pub static AES_128_GCM_SHA256: Tls13CipherSuite = Tls13CipherSuite { /// TLS 1.3 AES-256-GCM with SHA-384. pub static AES_256_GCM_SHA384: Tls13CipherSuite = Tls13CipherSuite { + protocol_version: rustls::version::TLS13_VERSION, common: rustls::crypto::CipherSuiteCommon { - suite: rustls::CipherSuite::TLS13_AES_256_GCM_SHA384, + suite: rustls::crypto::CipherSuite::TLS13_AES_256_GCM_SHA384, hash_provider: hash::SHA384, confidentiality_limit: 1 << 23, }, @@ -30,8 +32,9 @@ pub static AES_256_GCM_SHA384: Tls13CipherSuite = Tls13CipherSuite { /// /// Not available in FIPS mode. pub static CHACHA20_POLY1305_SHA256: Tls13CipherSuite = Tls13CipherSuite { + protocol_version: rustls::version::TLS13_VERSION, common: rustls::crypto::CipherSuiteCommon { - suite: rustls::CipherSuite::TLS13_CHACHA20_POLY1305_SHA256, + suite: rustls::crypto::CipherSuite::TLS13_CHACHA20_POLY1305_SHA256, hash_provider: hash::SHA256, confidentiality_limit: u64::MAX, }, diff --git a/src/verify.rs b/src/verify.rs index 30374c8..d19df76 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -1,4 +1,4 @@ -use rustls::{SignatureScheme, crypto::WebPkiSupportedAlgorithms}; +use rustls::crypto::{SignatureScheme, WebPkiSupportedAlgorithms}; pub(crate) mod ec; pub(crate) mod ed; diff --git a/src/verify/ec.rs b/src/verify/ec.rs index 828cc6f..e5376c8 100644 --- a/src/verify/ec.rs +++ b/src/verify/ec.rs @@ -1,5 +1,5 @@ use boring::{error::ErrorStack, hash::MessageDigest}; -use rustls::{SignatureScheme, pki_types::alg_id}; +use rustls::{crypto::SignatureScheme, pki_types::alg_id}; use rustls_pki_types::{InvalidSignature, SignatureVerificationAlgorithm}; use crate::helper; diff --git a/src/verify/ed.rs b/src/verify/ed.rs index 250115d..d0ff977 100644 --- a/src/verify/ed.rs +++ b/src/verify/ed.rs @@ -2,7 +2,7 @@ use std::ptr; use boring::error::ErrorStack; use foreign_types::ForeignType; -use rustls::{SignatureScheme, pki_types::alg_id}; +use rustls::{crypto::SignatureScheme, pki_types::alg_id}; use rustls_pki_types::{InvalidSignature, SignatureVerificationAlgorithm}; use crate::helper::{cvt_p, log_and_map}; diff --git a/src/verify/rsa.rs b/src/verify/rsa.rs index 06d15f2..1791150 100644 --- a/src/verify/rsa.rs +++ b/src/verify/rsa.rs @@ -5,9 +5,8 @@ use boring::{ rsa::{Padding, Rsa}, sign::RsaPssSaltlen, }; -use rustls::{SignatureScheme, pki_types::alg_id}; +use rustls::{crypto::SignatureScheme, pki_types::alg_id}; use rustls_pki_types::{InvalidSignature, SignatureVerificationAlgorithm}; -use spki::der::Reader; use crate::helper::log_and_map; @@ -123,15 +122,21 @@ pub(crate) fn decode_spki_spk( // public_key: unfortunately this is not a whole SPKI, but just the key material. // decode the two integers manually. - let mut reader = spki::der::SliceReader::new(spki_spk) - .map_err(|e| log_and_map("SliceReader::new", e, InvalidSignature))?; - let ne: [spki::der::asn1::UintRef; 2] = reader - .decode() - .map_err(|e| log_and_map("SliceReader::decode", e, InvalidSignature))?; + use spki::der::{Decode, Reader, SliceReader}; - let n = BigNum::from_slice(ne[0].as_bytes()) + let mut reader = SliceReader::new(spki_spk) + .map_err(|e| log_and_map("SliceReader::new", e, InvalidSignature))?; + let (n_ref, e_ref) = reader + .sequence(|inner: &mut spki::der::SliceReader<'_>| { + let n = spki::der::asn1::UintRef::decode(inner)?; + let e = spki::der::asn1::UintRef::decode(inner)?; + Ok((n, e)) + }) + .map_err(|e: spki::der::Error| log_and_map("sequence decode", e, InvalidSignature))?; + + let n = BigNum::from_slice(n_ref.as_bytes()) .map_err(|e| log_and_map("BigNum::from_slice", e, InvalidSignature))?; - let e = BigNum::from_slice(ne[1].as_bytes()) + let e = BigNum::from_slice(e_ref.as_bytes()) .map_err(|e| log_and_map("BigNum::from_slice", e, InvalidSignature))?; PKey::from_rsa( diff --git a/tests/e2e.rs b/tests/e2e.rs index 8393b7c..dc7ba18 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -1,44 +1,44 @@ -use rcgen::CertificateParams; use std::sync::Arc; -use tokio::{ - io::{AsyncReadExt, AsyncWriteExt}, - net::TcpStream, -}; use boring_rustls_provider::tls13; -#[cfg(feature = "tls12")] -use rustls::version::TLS12; -use rustls::{ClientConfig, ServerConfig, SupportedCipherSuite, version::TLS13}; -use rustls_pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}; -use tokio::net::TcpListener; -use tokio_rustls::{TlsAcceptor, TlsConnector}; +#[cfg(any(feature = "tls12", feature = "fips"))] +use rustls::crypto::SignatureScheme; +use rustls::crypto::kx::NamedGroup; +use rustls::crypto::{CipherSuite, Identity}; +use rustls::{ClientConfig, ServerConfig, SupportedCipherSuite}; +#[cfg(not(feature = "fips"))] +use rustls_pki_types::FipsStatus; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- -fn tls13_provider_suites() -> Vec { +fn all_tls13_suites() -> Vec<&'static rustls::Tls13CipherSuite> { boring_rustls_provider::provider() - .cipher_suites - .into_iter() - .filter(|suite| matches!(suite, SupportedCipherSuite::Tls13(_))) - .collect() + .tls13_cipher_suites + .to_vec() } #[cfg(feature = "tls12")] -fn tls12_provider_suites_for_ecdsa() -> Vec { +fn all_tls12_suites() -> Vec<&'static rustls::Tls12CipherSuite> { boring_rustls_provider::provider() - .cipher_suites + .tls12_cipher_suites + .to_vec() +} + +#[cfg(feature = "tls12")] +fn tls12_suites_for_ecdsa() -> Vec<&'static rustls::Tls12CipherSuite> { + all_tls12_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 + SignatureScheme::ECDSA_NISTP256_SHA256 + | SignatureScheme::ECDSA_NISTP384_SHA384 + | SignatureScheme::ECDSA_NISTP521_SHA512 + | SignatureScheme::ED25519 + | SignatureScheme::ED448 ) }) }) @@ -46,242 +46,308 @@ fn tls12_provider_suites_for_ecdsa() -> Vec { } #[cfg(feature = "tls12")] -fn tls12_provider_suites_for_rsa() -> Vec { - boring_rustls_provider::provider() - .cipher_suites +fn tls12_suites_for_rsa() -> Vec<&'static rustls::Tls12CipherSuite> { + all_tls12_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 + SignatureScheme::RSA_PKCS1_SHA256 + | SignatureScheme::RSA_PKCS1_SHA384 + | SignatureScheme::RSA_PKCS1_SHA512 + | SignatureScheme::RSA_PSS_SHA256 + | SignatureScheme::RSA_PSS_SHA384 + | SignatureScheme::RSA_PSS_SHA512 ) }) }) .collect() } -#[tokio::test] -async fn test_tls13_crypto() { - let pki = TestPki::new(&rcgen::PKCS_ECDSA_P256_SHA256); - - let root_store = pki.client_root_store(); - let server_config = pki.server_config(); +/// Perform a full TLS handshake and data exchange between a client and server +/// in memory (no network). Returns the negotiated key exchange group. +fn do_handshake( + client_config: ClientConfig, + server_config: Arc, +) -> Option { + let mut client = + rustls::ClientConnection::new(Arc::new(client_config), "localhost".try_into().unwrap()) + .unwrap(); + let mut server = rustls::ServerConnection::new(server_config).unwrap(); - let ciphers = tls13_provider_suites(); + // Drive the handshake to completion + let mut buf = Vec::new(); + loop { + // client -> server + buf.clear(); + if client.wants_write() { + client.write_tls(&mut buf).unwrap(); + } + if !buf.is_empty() { + server.read_tls(&mut &buf[..]).unwrap(); + server.process_new_packets().unwrap(); + } - for cipher in ciphers { - 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(); + // server -> client + buf.clear(); + if server.wants_write() { + server.write_tls(&mut buf).unwrap(); + } + if !buf.is_empty() { + client.read_tls(&mut &buf[..]).unwrap(); + client.process_new_packets().unwrap(); + } - do_exchange(config, server_config.clone()).await; + if !client.is_handshaking() && !server.is_handshaking() { + break; + } } + + // Exchange application data + use std::io::{Read, Write}; + client.writer().write_all(b"HELLO").unwrap(); + + buf.clear(); + client.write_tls(&mut buf).unwrap(); + server.read_tls(&mut &buf[..]).unwrap(); + server.process_new_packets().unwrap(); + + let mut app_buf = [0u8; 5]; + server.reader().read_exact(&mut app_buf).unwrap(); + assert_eq!(&app_buf, b"HELLO"); + + client + .negotiated_key_exchange_group() + .map(|group| group.name()) } -#[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"); +struct TestPki { + ca_cert_der: rustls_pki_types::CertificateDer<'static>, + server_cert_der: rustls_pki_types::CertificateDer<'static>, + server_key_der: rustls_pki_types::PrivateKeyDer<'static>, +} - 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() - ); +impl TestPki { + fn new(alg: &'static rcgen::SignatureAlgorithm) -> Self { + let ca_key = keypair_for_alg(alg); + let mut ca_params = rcgen::CertificateParams::new(Vec::::new()).unwrap(); + ca_params + .distinguished_name + .push(rcgen::DnType::OrganizationName, "Provider Server Example"); + ca_params + .distinguished_name + .push(rcgen::DnType::CommonName, "Example CA"); + ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained); + ca_params.key_usages = vec![ + rcgen::KeyUsagePurpose::KeyCertSign, + rcgen::KeyUsagePurpose::DigitalSignature, + ]; + let ca = rcgen::CertifiedIssuer::self_signed(ca_params, &ca_key).unwrap(); + let ca_cert_der = ca.der().clone(); + + let server_key = keypair_for_alg(alg); + let mut server_params = + rcgen::CertificateParams::new(vec!["localhost".to_string()]).unwrap(); + server_params.is_ca = rcgen::IsCa::NoCa; + server_params.extended_key_usages = vec![rcgen::ExtendedKeyUsagePurpose::ServerAuth]; + let server_cert = server_params.signed_by(&server_key, &ca).unwrap(); + let server_cert_der = server_cert.der().clone(); + let server_key_der = + rustls_pki_types::PrivatePkcs8KeyDer::from(server_key.serialize_der()).into(); + + Self { + ca_cert_der, + server_cert_der, + server_key_der, + } + } + + fn client_root_store(&self) -> rustls::RootCertStore { + let mut root_store = rustls::RootCertStore::empty(); + root_store.add(self.ca_cert_der.clone()).unwrap(); + root_store + } + + fn server_identity(&self) -> Arc> { + Arc::new( + Identity::from_cert_chain(vec![self.server_cert_der.clone()]) + .expect("valid cert chain"), + ) + } + + fn server_config(&self) -> Arc { + let mut cfg = ServerConfig::builder(Arc::new(boring_rustls_provider::provider())) + .with_no_client_auth() + .with_single_cert(self.server_identity(), self.server_key_der.clone_key()) + .unwrap(); + cfg.key_log = Arc::new(rustls::KeyLogFile::new()); + Arc::new(cfg) + } +} + +fn keypair_for_alg(alg: &'static rcgen::SignatureAlgorithm) -> rcgen::KeyPair { + if alg == &rcgen::PKCS_RSA_SHA256 { + gen_rsa_key(alg, 2048) + } else if alg == &rcgen::PKCS_RSA_SHA384 { + gen_rsa_key(alg, 3072) + } else if alg == &rcgen::PKCS_RSA_SHA512 { + gen_rsa_key(alg, 4096) + } else { + rcgen::KeyPair::generate_for(alg).unwrap() } } +fn gen_rsa_key(alg: &'static rcgen::SignatureAlgorithm, bits: u32) -> rcgen::KeyPair { + let rsa = boring::rsa::Rsa::generate(bits).unwrap(); + let der_pkcs8 = boring::pkey::PKey::from_rsa(rsa) + .unwrap() + .private_key_to_der_pkcs8() + .unwrap(); + let key_der: rustls_pki_types::PrivateKeyDer<'static> = + rustls_pki_types::PrivatePkcs8KeyDer::from(der_pkcs8).into(); + rcgen::KeyPair::from_der_and_sign_algo(&key_der, alg).unwrap() +} + +// --------------------------------------------------------------------------- +// TLS handshake tests +// --------------------------------------------------------------------------- + #[test] -fn provider_verifiers_reject_malformed_inputs_without_panicking() { - let provider = boring_rustls_provider::provider(); +fn test_tls13_crypto() { + let pki = TestPki::new(&rcgen::PKCS_ECDSA_P256_SHA256); + let root_store = pki.client_root_store(); + let server_config = pki.server_config(); - 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", &[]) - })); + for suite in all_tls13_suites() { + let config = ClientConfig::builder(Arc::new( + boring_rustls_provider::provider_with_ciphers(vec![SupportedCipherSuite::Tls13(suite)]), + )) + .with_root_certificates(root_store.clone()) + .with_no_client_auth() + .unwrap(); - assert!(outcome.is_ok(), "verifier #{index} panicked"); - assert!( - outcome.expect("already checked for panic").is_err(), - "verifier #{index} accepted malformed inputs" - ); + do_handshake(config, server_config.clone()); } } -/// 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; - +#[test] +fn test_tls13_pq_x25519_mlkem768() { 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() + let mut cfg = ServerConfig::builder(Arc::new(server_provider)) .with_no_client_auth() - .with_single_cert( - vec![pki.server_cert_der.clone()], - pki.server_key_der.clone_key(), - ) + .with_single_cert(pki.server_identity(), 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() + let config = ClientConfig::builder(Arc::new(client_provider)) .with_root_certificates(root_store) - .with_no_client_auth(); + .with_no_client_auth() + .unwrap(); - let negotiated_group = do_exchange(config, server_config).await; + let negotiated_group = do_handshake(config, server_config); 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(); +#[cfg(feature = "tls12")] +#[test] +fn test_tls12_ec_crypto() { + let pki = TestPki::new(&rcgen::PKCS_ECDSA_P256_SHA256); + let root_store = pki.client_root_store(); + let server_config = pki.server_config(); - let connector = TlsConnector::from(Arc::new(config)); - let stream = TcpStream::connect("pq.cloudflareresearch.com:443") - .await + for suite in tls12_suites_for_ecdsa() { + let config = ClientConfig::builder(Arc::new( + boring_rustls_provider::provider_with_ciphers(vec![SupportedCipherSuite::Tls12(suite)]), + )) + .with_root_certificates(root_store.clone()) + .with_no_client_auth() .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(); + do_handshake(config, server_config.clone()); + } +} - let mut buf = Vec::new(); - stream.read_to_end(&mut buf).await.unwrap(); - let response = String::from_utf8_lossy(&buf); +#[cfg(feature = "tls12")] +#[test] +fn test_tls12_rsa_crypto() { + let pki = TestPki::new(&rcgen::PKCS_RSA_SHA256); + let root_store = pki.client_root_store(); + let server_config = pki.server_config(); - // Verify TLS 1.3 was used - assert!( - response.contains("tls=TLSv1.3"), - "expected TLSv1.3, got: {response}" - ); + for suite in tls12_suites_for_rsa() { + let config = ClientConfig::builder(Arc::new( + boring_rustls_provider::provider_with_ciphers(vec![SupportedCipherSuite::Tls12(suite)]), + )) + .with_root_certificates(root_store.clone()) + .with_no_client_auth() + .unwrap(); - // Verify X25519MLKEM768 was negotiated as the key exchange - assert!( - response.contains("kex=X25519MLKEM768"), - "expected kex=X25519MLKEM768, got: {response}" - ); + do_handshake(config, server_config.clone()); + } } -/// 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()); +// --------------------------------------------------------------------------- +// Provider unit tests +// --------------------------------------------------------------------------- - 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"); +#[test] +fn provider_kx_groups_reject_invalid_peer_keys_without_panicking() { + for group in boring_rustls_provider::provider().kx_groups.iter() { + let kx = group.start().expect("provider KX group should initialize"); - 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 outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + kx.into_single().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() + ); + } +} - let mut buf = Vec::new(); - stream.read_to_end(&mut buf).await.unwrap(); - let response = String::from_utf8_lossy(&buf); +#[test] +fn provider_verifiers_reject_malformed_inputs_without_panicking() { + let provider = boring_rustls_provider::provider(); - // Verify TLS 1.2 was used - assert!( - response.contains("tls=TLSv1.2"), - "expected TLSv1.2, got: {response}" - ); + 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", &[]) + })); - // 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}" - ); + assert!(outcome.is_ok(), "verifier #{index} panicked"); + assert!( + outcome.expect("already checked for panic").is_err(), + "verifier #{index} accepted malformed inputs" + ); + } } +// --------------------------------------------------------------------------- +// FIPS tests +// --------------------------------------------------------------------------- + #[test] #[cfg(feature = "fips")] fn is_fips_enabled() { @@ -291,8 +357,6 @@ fn is_fips_enabled() { #[test] #[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, @@ -300,11 +364,18 @@ fn fips_provider_excludes_chacha20_cipher_suites() { CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, ]; - for suite in provider.cipher_suites { - let selected = suite.suite(); + for suite in provider.tls13_cipher_suites.iter() { + let selected = suite.common.suite; assert!( !disallowed.contains(&selected), - "FIPS provider exposed disallowed cipher suite: {selected:?}" + "FIPS provider exposed disallowed TLS 1.3 cipher suite: {selected:?}" + ); + } + for suite in provider.tls12_cipher_suites.iter() { + let selected = suite.common.suite; + assert!( + !disallowed.contains(&selected), + "FIPS provider exposed disallowed TLS 1.2 cipher suite: {selected:?}" ); } } @@ -312,18 +383,16 @@ 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 + let suites: Vec<_> = provider + .tls13_cipher_suites .iter() - .map(|suite| suite.suite()) - .collect::>(); + .map(|suite| suite.common.suite) + .collect(); assert_eq!(suites, vec![CipherSuite::TLS13_AES_128_GCM_SHA256]); } @@ -331,16 +400,13 @@ fn fips_provider_with_ciphers_filters_non_fips_input() { #[test] #[cfg(feature = "fips")] fn fips_provider_restricts_kx_groups() { - use rustls::NamedGroup; - let provider = boring_rustls_provider::provider(); - let groups = provider + let groups: Vec<_> = provider .kx_groups .iter() .map(|group| group.name()) - .collect::>(); + .collect(); - // fips implies mlkem, so X25519MLKEM768 must be present and preferred assert_eq!( groups[0], NamedGroup::X25519MLKEM768, @@ -362,15 +428,13 @@ fn fips_provider_restricts_kx_groups() { #[test] #[cfg(feature = "fips")] fn fips_provider_excludes_disallowed_signature_schemes() { - use rustls::SignatureScheme; - let provider = boring_rustls_provider::provider(); - let schemes = provider + let schemes: Vec<_> = provider .signature_verification_algorithms .mapping .iter() .map(|(scheme, _)| *scheme) - .collect::>(); + .collect(); assert!(schemes.contains(&SignatureScheme::RSA_PSS_SHA256)); assert!(schemes.contains(&SignatureScheme::ECDSA_NISTP256_SHA256)); @@ -387,6 +451,10 @@ fn fips_provider_excludes_disallowed_signature_schemes() { } } +// --------------------------------------------------------------------------- +// Non-FIPS tests +// --------------------------------------------------------------------------- + #[test] #[cfg(not(feature = "fips"))] fn is_fips_disabled() { @@ -396,15 +464,13 @@ fn is_fips_disabled() { #[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 + .tls13_cipher_suites .iter() - .any(|suite| { suite.suite() == CipherSuite::TLS13_CHACHA20_POLY1305_SHA256 }) + .any(|suite| suite.common.suite == CipherSuite::TLS13_CHACHA20_POLY1305_SHA256) ); assert!( @@ -420,25 +486,23 @@ fn non_fips_provider_keeps_non_fips_algorithms() { fn non_fips_provider_components_report_non_fips() { let provider = boring_rustls_provider::provider(); - assert!(!provider.secure_random.fips()); - assert!(!provider.key_provider.fips()); + assert_eq!(provider.secure_random.fips(), FipsStatus::Unvalidated); + assert_eq!(provider.key_provider.fips(), FipsStatus::Unvalidated); } #[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 + let suites: Vec<_> = provider + .tls13_cipher_suites .iter() - .map(|suite| suite.suite()) - .collect::>(); + .map(|suite| suite.common.suite) + .collect(); assert_eq!( suites, @@ -452,14 +516,12 @@ fn non_fips_provider_with_ciphers_keeps_requested_suites() { #[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 + let groups: Vec<_> = provider .kx_groups .iter() .map(|group| group.name()) - .collect::>(); + .collect(); assert_eq!( groups[0], @@ -477,189 +539,3 @@ fn non_fips_provider_includes_pq_group() { "P-256 should follow X25519, matching boring's default order" ); } - -#[cfg(feature = "tls12")] -#[tokio::test] -async fn test_tls12_ec_crypto() { - let pki = TestPki::new(&rcgen::PKCS_ECDSA_P256_SHA256); - - let root_store = pki.client_root_store(); - let server_config = pki.server_config(); - - let ciphers = tls12_provider_suites_for_ecdsa(); - - for cipher in ciphers { - 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; - } -} - -#[cfg(feature = "tls12")] -#[tokio::test] -async fn test_tls12_rsa_crypto() { - let pki = TestPki::new(&rcgen::PKCS_RSA_SHA256); - - let root_store = pki.client_root_store(); - let server_config = pki.server_config(); - - let ciphers = tls12_provider_suites_for_rsa(); - - for cipher in ciphers { - 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; - } -} - -async fn new_listener() -> TcpListener { - TcpListener::bind("localhost:0").await.unwrap() -} - -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())); - - let connector = TlsConnector::from(Arc::new(config)); - let stream = TcpStream::connect(&addr).await.unwrap(); - - let mut stream = connector - .connect( - rustls_pki_types::ServerName::try_from("localhost").unwrap(), - stream, - ) - .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) { - let acceptor = TlsAcceptor::from(config); - - let (stream, _) = listener.accept().await.unwrap(); - let acceptor = acceptor.clone(); - let mut stream = acceptor.accept(stream).await.unwrap(); - - let mut buf = vec![0u8; 5]; - let bytes = stream.read_exact(buf.as_mut_slice()).await.unwrap(); - stream.write_all(&buf[..bytes]).await.unwrap(); - stream.flush().await.unwrap(); - stream.shutdown().await.unwrap(); -} - -struct TestPki { - ca_cert_der: CertificateDer<'static>, - server_cert_der: CertificateDer<'static>, - server_key_der: PrivateKeyDer<'static>, -} - -impl TestPki { - fn new(alg: &'static rcgen::SignatureAlgorithm) -> Self { - let mut ca_params = rcgen::CertificateParams::new(Vec::new()); - ca_params - .distinguished_name - .push(rcgen::DnType::OrganizationName, "Provider Server Example"); - ca_params - .distinguished_name - .push(rcgen::DnType::CommonName, "Example CA"); - ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained); - ca_params.key_usages = vec![ - rcgen::KeyUsagePurpose::KeyCertSign, - rcgen::KeyUsagePurpose::DigitalSignature, - ]; - keypair_for_alg(&mut ca_params, alg); - ca_params.alg = alg; - let ca_cert = rcgen::Certificate::from_params(ca_params).unwrap(); - - let ca_cert_der = CertificateDer::from(ca_cert.serialize_der().unwrap()); - // Create a server end entity cert issued by the CA. - let mut server_ee_params = rcgen::CertificateParams::new(vec!["localhost".to_string()]); - server_ee_params.is_ca = rcgen::IsCa::NoCa; - server_ee_params.extended_key_usages = vec![rcgen::ExtendedKeyUsagePurpose::ServerAuth]; - server_ee_params.alg = alg; - keypair_for_alg(&mut server_ee_params, alg); - let server_cert = rcgen::Certificate::from_params(server_ee_params).unwrap(); - let server_cert_der = - CertificateDer::from(server_cert.serialize_der_with_signer(&ca_cert).unwrap()); - let server_key_der = - PrivatePkcs8KeyDer::from(server_cert.serialize_private_key_der()).into(); - Self { - ca_cert_der, - server_cert_der, - server_key_der, - } - } - - 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(versions) - .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()); - - Arc::new(server_config) - } - - fn client_root_store(&self) -> rustls::RootCertStore { - let mut root_store = rustls::RootCertStore::empty(); - root_store.add(self.ca_cert_der.clone()).unwrap(); - root_store - } -} - -fn gen_rsa_key(bits: u32) -> rcgen::KeyPair { - let rsa = boring::rsa::Rsa::generate(bits).unwrap(); - - let der_pkcs8 = boring::pkey::PKey::from_rsa(rsa) - .unwrap() - .private_key_to_der_pkcs8() - .unwrap(); - rcgen::KeyPair::from_der(&der_pkcs8).unwrap() -} - -fn keypair_for_alg(params: &mut CertificateParams, alg: &rcgen::SignatureAlgorithm) { - if alg == &rcgen::PKCS_RSA_SHA256 { - params.key_pair = Some(gen_rsa_key(2048)); - } else if alg == &rcgen::PKCS_RSA_SHA384 { - params.key_pair = Some(gen_rsa_key(3072)); - } else if alg == &rcgen::PKCS_RSA_SHA512 { - params.key_pair = Some(gen_rsa_key(4096)); - } -}