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.
This commit is contained in:
Jan Rüth 2026-04-10 16:30:01 +02:00 committed by Jan
commit b88c87235d
19 changed files with 1235 additions and 277 deletions

View file

@ -78,11 +78,7 @@ impl Crypter {
/// ///
/// # Errors /// # Errors
/// Returns the `BoringSSL` error in case of an internal error /// 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<Self, ErrorStack> { pub fn new(aead_alg: &Algorithm, key: &[u8]) -> Result<Self, ErrorStack> {
assert_eq!(aead_alg.key_length(), key.len());
boring_sys::init(); boring_sys::init();
let this = unsafe { let this = unsafe {
@ -116,10 +112,6 @@ impl Crypter {
/// ///
/// # Errors /// # Errors
/// In case of an error, returns the `BoringSSL` error /// 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( pub fn seal_in_place(
&self, &self,
nonce: &[u8], nonce: &[u8],
@ -127,8 +119,9 @@ impl Crypter {
buffer: &mut [u8], buffer: &mut [u8],
tag: &mut [u8], tag: &mut [u8],
) -> Result<usize, ErrorStack> { ) -> Result<usize, ErrorStack> {
assert!(tag.len() >= self.max_overhead); if tag.len() < self.max_overhead || nonce.len() != self.nonce_len {
assert_eq!(nonce.len(), self.nonce_len); return Err(ErrorStack::get());
}
let mut tag_len = tag.len(); let mut tag_len = tag.len();
unsafe { unsafe {
@ -156,9 +149,6 @@ impl Crypter {
/// ///
/// # Errors /// # Errors
/// In case of an error, returns the `BoringSSL` error /// In case of an error, returns the `BoringSSL` error
///
/// # Panics
/// * if the nonce has the wrong lenght
pub fn open_in_place( pub fn open_in_place(
&self, &self,
nonce: &[u8], nonce: &[u8],
@ -166,7 +156,9 @@ impl Crypter {
buffer: &mut [u8], buffer: &mut [u8],
tag: &[u8], tag: &[u8],
) -> Result<(), ErrorStack> { ) -> Result<(), ErrorStack> {
assert_eq!(nonce.len(), self.nonce_len); if nonce.len() != self.nonce_len {
return Err(ErrorStack::get());
}
unsafe { unsafe {
cvt(boring_sys::EVP_AEAD_CTX_open_gather( cvt(boring_sys::EVP_AEAD_CTX_open_gather(
@ -188,11 +180,11 @@ impl Crypter {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::Crypter; use super::{Algorithm, Crypter};
#[test] #[test]
fn in_out() { 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 nonce = [0u8; 12];
let associated_data = b"this is authenticated"; let associated_data = b"this is authenticated";
let mut buffer = Vec::with_capacity(26); let mut buffer = Vec::with_capacity(26);
@ -207,4 +199,35 @@ mod tests {
assert_eq!(b"ABCDE", buffer.as_slice()); 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());
}
} }

View file

@ -5,8 +5,6 @@ use std::{
use foreign_types::{ForeignType, ForeignTypeRef, Opaque}; use foreign_types::{ForeignType, ForeignTypeRef, Opaque};
use crate::helper::{cvt, cvt_p};
pub struct HmacCtxRef(Opaque); pub struct HmacCtxRef(Opaque);
unsafe impl ForeignTypeRef for HmacCtxRef { 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 { impl Drop for HmacCtx {
fn drop(&mut self) { fn drop(&mut self) {
unsafe { unsafe {

View file

@ -35,6 +35,9 @@ pub(crate) trait BoringCipher {
/// confidentiality limit /// confidentiality limit
const CONFIDENTIALITY_LIMIT: u64; 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 /// Constructs a new instance of this cipher as an AEAD algorithm
fn new_cipher() -> Algorithm; fn new_cipher() -> Algorithm;
@ -49,7 +52,7 @@ pub(crate) trait QuicCipher: Send + Sync {
/// the expected length of a sample /// the expected length of a sample
const SAMPLE_LEN: usize; 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 {} pub(crate) trait BoringAead: BoringCipher + AeadCore + Send + Sync {}
@ -74,19 +77,21 @@ impl<T: BoringAead> AeadCore for BoringAeadCrypter<T> {
impl<T: BoringAead> BoringAeadCrypter<T> { impl<T: BoringAead> BoringAeadCrypter<T> {
/// Creates a new aead crypter /// Creates a new aead crypter
pub fn new(iv: Iv, key: &[u8], tls_version: ProtocolVersion) -> Result<Self, ErrorStack> { pub fn new(iv: Iv, key: &[u8], tls_version: ProtocolVersion) -> Result<Self, ErrorStack> {
assert!(match tls_version { let tls_version_supported = match tls_version {
#[cfg(feature = "tls12")] #[cfg(feature = "tls12")]
ProtocolVersion::TLSv1_2 => true, ProtocolVersion::TLSv1_2 => true,
ProtocolVersion::TLSv1_3 => true, ProtocolVersion::TLSv1_3 => true,
_ => false, _ => false,
}); };
if !tls_version_supported {
return Err(ErrorStack::get());
}
let cipher = <T as BoringCipher>::new_cipher(); let cipher = <T as BoringCipher>::new_cipher();
assert_eq!( if cipher.nonce_len() != rustls::crypto::cipher::Nonce::new(&iv, 0).0.len() {
cipher.nonce_len(), return Err(ErrorStack::get());
rustls::crypto::cipher::Nonce::new(&iv, 0).0.len() }
);
let crypter = BoringAeadCrypter { let crypter = BoringAeadCrypter {
crypter: boring_additions::aead::Crypter::new(&cipher, key)?, 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 { fn encrypted_payload_len(&self, payload_len: usize) -> usize {
match self.tls_version { let per_record_overhead = match self.tls_version {
ProtocolVersion::TLSv1_2 => { ProtocolVersion::TLSv1_2 => self
payload_len + self.crypter.max_overhead() + <T as BoringCipher>::EXPLICIT_NONCE_LEN .crypter
} .max_overhead()
ProtocolVersion::TLSv1_3 => payload_len + 1 + self.crypter.max_overhead(), .saturating_add(<T as BoringCipher>::EXPLICIT_NONCE_LEN),
_ => unimplemented!(), 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")] #[cfg(feature = "tls12")]
ProtocolVersion::TLSv1_2 => { ProtocolVersion::TLSv1_2 => {
let explicit_nonce_len = <T as BoringCipher>::EXPLICIT_NONCE_LEN; let explicit_nonce_len = <T as BoringCipher>::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] // payload is: [nonce] | [ciphertext] | [auth tag]
let actual_payload_length = let actual_payload_length = m.payload.len() - tag_len - 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 aad = make_tls12_aad(seq, m.typ, m.version, actual_payload_length);
@ -224,7 +239,9 @@ where
let nonce = { let nonce = {
let fixed_iv_len = <T as BoringCipher>::FIXED_IV_LEN; let fixed_iv_len = <T as BoringCipher>::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 // grab the IV by constructing a nonce, this is just an xor
let iv = cipher::Nonce::new(&self.iv, seq).0; let iv = cipher::Nonce::new(&self.iv, seq).0;
@ -235,8 +252,7 @@ where
}; };
// split off the authentication tag // split off the authentication tag
let (payload, tag) = let (payload, tag) = payload.split_at_mut(payload.len() - tag_len);
payload.split_at_mut(payload.len() - self.crypter.max_overhead());
self.crypter self.crypter
.open_in_place(&nonce, &aad, payload, tag) .open_in_place(&nonce, &aad, payload, tag)
@ -262,7 +278,7 @@ where
.map_err(|_| rustls::Error::DecryptError) .map_err(|_| rustls::Error::DecryptError)
.and_then(|_| m.into_tls13_unpadded_message()) .and_then(|_| m.into_tls13_unpadded_message())
} }
_ => unimplemented!(), _ => Err(rustls::Error::DecryptError),
} }
} }
} }
@ -295,6 +311,10 @@ where
let associated_data = header; let associated_data = header;
let nonce = cipher::Nonce::new(&self.iv, packet_number); 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()); let (buffer, tag) = payload.split_at_mut(payload.len() - self.crypter.max_overhead());
self.crypter self.crypter
@ -317,6 +337,68 @@ where
} }
} }
struct InvalidMessageEncrypter;
impl cipher::MessageEncrypter for InvalidMessageEncrypter {
fn encrypt(
&mut self,
_msg: cipher::OutboundPlainMessage<'_>,
_seq: u64,
) -> Result<cipher::OutboundOpaqueMessage, rustls::Error> {
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<cipher::InboundPlainMessage<'a>, rustls::Error> {
Err(rustls::Error::DecryptError)
}
}
struct InvalidPacketKey<T>(PhantomData<T>);
impl<T: BoringCipher + QuicCipher> rustls::quic::PacketKey for InvalidPacketKey<T> {
fn encrypt_in_place(
&self,
_packet_number: u64,
_header: &[u8],
_payload: &mut [u8],
) -> Result<rustls::quic::Tag, rustls::Error> {
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 {
<T as BoringCipher>::TAG_LEN
}
fn confidentiality_limit(&self) -> u64 {
0
}
fn integrity_limit(&self) -> u64 {
0
}
}
pub(crate) struct Aead<T>(PhantomData<T>); pub(crate) struct Aead<T>(PhantomData<T>);
impl<T> Aead<T> { impl<T> Aead<T> {
@ -325,17 +407,31 @@ impl<T> Aead<T> {
impl<T: BoringAead + 'static> cipher::Tls13AeadAlgorithm for Aead<T> { impl<T: BoringAead + 'static> cipher::Tls13AeadAlgorithm for Aead<T> {
fn encrypter(&self, key: cipher::AeadKey, iv: cipher::Iv) -> Box<dyn cipher::MessageEncrypter> { fn encrypter(&self, key: cipher::AeadKey, iv: cipher::Iv) -> Box<dyn cipher::MessageEncrypter> {
Box::new( match BoringAeadCrypter::<T>::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3) {
BoringAeadCrypter::<T>::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3) Ok(crypter) => Box::new(crypter),
.expect("failed to create AEAD crypter"), Err(err) => {
) log_and_map(
"Tls13AeadAlgorithm::encrypter BoringAeadCrypter::new",
err,
(),
);
Box::new(InvalidMessageEncrypter)
}
}
} }
fn decrypter(&self, key: cipher::AeadKey, iv: cipher::Iv) -> Box<dyn cipher::MessageDecrypter> { fn decrypter(&self, key: cipher::AeadKey, iv: cipher::Iv) -> Box<dyn cipher::MessageDecrypter> {
Box::new( match BoringAeadCrypter::<T>::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3) {
BoringAeadCrypter::<T>::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3) Ok(crypter) => Box::new(crypter),
.expect("failed to create AEAD crypter"), Err(err) => {
) log_and_map(
"Tls13AeadAlgorithm::decrypter BoringAeadCrypter::new",
err,
(),
);
Box::new(InvalidMessageDecrypter)
}
}
} }
fn key_len(&self) -> usize { fn key_len(&self) -> usize {
@ -349,6 +445,10 @@ impl<T: BoringAead + 'static> cipher::Tls13AeadAlgorithm for Aead<T> {
) -> Result<ConnectionTrafficSecrets, cipher::UnsupportedOperationError> { ) -> Result<ConnectionTrafficSecrets, cipher::UnsupportedOperationError> {
Ok(<T as BoringCipher>::extract_keys(key, iv)) Ok(<T as BoringCipher>::extract_keys(key, iv))
} }
fn fips(&self) -> bool {
cfg!(feature = "fips") && <T as BoringCipher>::FIPS_APPROVED
}
} }
#[cfg(feature = "tls12")] #[cfg(feature = "tls12")]
@ -362,24 +462,42 @@ impl<T: BoringAead + 'static> cipher::Tls12AeadAlgorithm for Aead<T> {
let mut full_iv = Vec::with_capacity(iv.len() + extra.len()); let mut full_iv = Vec::with_capacity(iv.len() + extra.len());
full_iv.extend_from_slice(iv); full_iv.extend_from_slice(iv);
full_iv.extend_from_slice(extra); full_iv.extend_from_slice(extra);
Box::new( match BoringAeadCrypter::<T>::new(
BoringAeadCrypter::<T>::new(Iv::copy(&full_iv), key.as_ref(), ProtocolVersion::TLSv1_2) Iv::copy(&full_iv),
.expect("failed to create AEAD crypter"), 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<dyn cipher::MessageDecrypter> { fn decrypter(&self, key: cipher::AeadKey, iv: &[u8]) -> Box<dyn cipher::MessageDecrypter> {
let mut pseudo_iv = Vec::with_capacity(iv.len() + <T as BoringCipher>::EXPLICIT_NONCE_LEN); let mut pseudo_iv = Vec::with_capacity(iv.len() + <T as BoringCipher>::EXPLICIT_NONCE_LEN);
pseudo_iv.extend_from_slice(iv); pseudo_iv.extend_from_slice(iv);
pseudo_iv.extend_from_slice(&vec![0u8; <T as BoringCipher>::EXPLICIT_NONCE_LEN]); pseudo_iv.extend_from_slice(&vec![0u8; <T as BoringCipher>::EXPLICIT_NONCE_LEN]);
Box::new( match BoringAeadCrypter::<T>::new(
BoringAeadCrypter::<T>::new(
Iv::copy(&pseudo_iv), Iv::copy(&pseudo_iv),
key.as_ref(), key.as_ref(),
ProtocolVersion::TLSv1_2, ProtocolVersion::TLSv1_2,
) ) {
.expect("failed to create AEAD crypter"), 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 { fn key_block_shape(&self) -> cipher::KeyBlockShape {
@ -399,7 +517,13 @@ impl<T: BoringAead + 'static> cipher::Tls12AeadAlgorithm for Aead<T> {
let nonce = { let nonce = {
let fixed_iv_len = <T as BoringCipher>::FIXED_IV_LEN; let fixed_iv_len = <T as BoringCipher>::FIXED_IV_LEN;
let explicit_nonce_len = <T as BoringCipher>::EXPLICIT_NONCE_LEN; let explicit_nonce_len = <T as BoringCipher>::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 // grab the IV by constructing a nonce, this is just an xor
@ -410,6 +534,10 @@ impl<T: BoringAead + 'static> cipher::Tls12AeadAlgorithm for Aead<T> {
}; };
Ok(<T as BoringCipher>::extract_keys(key, Iv::copy(&nonce))) Ok(<T as BoringCipher>::extract_keys(key, Iv::copy(&nonce)))
} }
fn fips(&self) -> bool {
cfg!(feature = "fips") && <T as BoringCipher>::FIPS_APPROVED
}
} }
struct QuicHeaderProtector<T: QuicCipher> { struct QuicHeaderProtector<T: QuicCipher> {
@ -425,8 +553,8 @@ impl<T: QuicCipher> QuicHeaderProtector<T> {
first: &mut u8, first: &mut u8,
packet_number: &mut [u8], packet_number: &mut [u8],
remove: bool, remove: bool,
) { ) -> Result<(), rustls::Error> {
let mask = T::header_protection_mask(self.key.as_ref(), sample); let mask = T::header_protection_mask(self.key.as_ref(), sample)?;
const LONG_HEADER_FORMAT: u8 = 0x80; const LONG_HEADER_FORMAT: u8 = 0x80;
let bits_to_mask = if (*first & LONG_HEADER_FORMAT) == LONG_HEADER_FORMAT { let bits_to_mask = if (*first & LONG_HEADER_FORMAT) == LONG_HEADER_FORMAT {
@ -453,6 +581,8 @@ impl<T: QuicCipher> QuicHeaderProtector<T> {
for (pn_byte, m) in packet_number.iter_mut().zip(&mask[1..]).take(pn_length) { for (pn_byte, m) in packet_number.iter_mut().zip(&mask[1..]).take(pn_length) {
*pn_byte ^= m; *pn_byte ^= m;
} }
Ok(())
} }
} }
@ -468,7 +598,7 @@ impl<T: QuicCipher> rustls::quic::HeaderProtectionKey for QuicHeaderProtector<T>
return Err(rustls::Error::General("packet number too long".into())); 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(()) Ok(())
} }
@ -483,7 +613,7 @@ impl<T: QuicCipher> rustls::quic::HeaderProtectionKey for QuicHeaderProtector<T>
return Err(rustls::Error::General("packet number too long".into())); 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(()) Ok(())
} }
@ -498,10 +628,13 @@ where
T: QuicCipher + BoringAead + 'static, T: QuicCipher + BoringAead + 'static,
{ {
fn packet_key(&self, key: cipher::AeadKey, iv: Iv) -> Box<dyn rustls::quic::PacketKey> { fn packet_key(&self, key: cipher::AeadKey, iv: Iv) -> Box<dyn rustls::quic::PacketKey> {
Box::new( match BoringAeadCrypter::<T>::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3) {
BoringAeadCrypter::<T>::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3) Ok(crypter) => Box::new(crypter),
.expect("failed to create AEAD crypter"), Err(err) => {
) log_and_map("QuicAlgorithm::packet_key BoringAeadCrypter::new", err, ());
Box::new(InvalidPacketKey::<T>(PhantomData))
}
}
} }
fn header_protection_key( fn header_protection_key(
@ -517,6 +650,10 @@ where
fn aead_key_len(&self) -> usize { fn aead_key_len(&self) -> usize {
<T as QuicCipher>::KEY_SIZE <T as QuicCipher>::KEY_SIZE
} }
fn fips(&self) -> bool {
cfg!(feature = "fips") && <T as BoringCipher>::FIPS_APPROVED
}
} }
struct DecryptBufferAdapter<'a, 'p>(&'a mut BorrowedPayload<'p>); struct DecryptBufferAdapter<'a, 'p>(&'a mut BorrowedPayload<'p>);
@ -535,7 +672,7 @@ impl AsMut<[u8]> for DecryptBufferAdapter<'_, '_> {
impl Buffer for DecryptBufferAdapter<'_, '_> { impl Buffer for DecryptBufferAdapter<'_, '_> {
fn extend_from_slice(&mut self, _: &[u8]) -> aead::Result<()> { 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) { fn truncate(&mut self, len: usize) {
@ -571,13 +708,20 @@ impl Buffer for EncryptBufferAdapter<'_> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use hex_literal::hex; use hex_literal::hex;
use rustls::crypto::cipher::AeadKey; use rustls::crypto::cipher::Tls13AeadAlgorithm;
use rustls::crypto::cipher::Iv; 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 crate::aead::BoringAeadCrypter;
use rustls::quic::PacketKey; use rustls::quic::PacketKey;
use super::{chacha20::ChaCha20Poly1305, QuicHeaderProtector}; use super::aes::Aes128;
use super::{chacha20::ChaCha20Poly1305, BoringCipher, QuicHeaderProtector};
#[test] #[test]
fn quic_header_protection_short() { fn quic_header_protection_short() {
@ -594,14 +738,97 @@ mod tests {
phantom: std::marker::PhantomData::<ChaCha20Poly1305>, phantom: std::marker::PhantomData::<ChaCha20Poly1305>,
}; };
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[..]); assert_eq!(&header[..], &protected_header[..]);
let (first, packet_number) = header.split_at_mut(1); 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[..]); 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::<ChaCha20Poly1305>,
};
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::<Aes128>::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::<Aes128>::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::<Aes128>::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] #[test]
fn quic_chacha20_crypt() { fn quic_chacha20_crypt() {
// test vector from https://www.rfc-editor.org/rfc/rfc9001.html#name-chacha20-poly1305-short-hea // 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::<ChaCha20Poly1305>::new( let protector = BoringAeadCrypter::<ChaCha20Poly1305>::new(
Iv::from(iv), Iv::from(iv),
&key, &key,
rustls::ProtocolVersion::TLSv1_3, ProtocolVersion::TLSv1_3,
) )
.unwrap(); .unwrap();
@ -635,4 +862,47 @@ mod tests {
assert_eq!(cleartext, expected_cleartext); assert_eq!(cleartext, expected_cleartext);
} }
#[test]
fn quic_decrypt_rejects_truncated_payload() {
let key = [0u8; <ChaCha20Poly1305 as BoringCipher>::KEY_SIZE];
let iv = [0u8; 12];
let crypter = BoringAeadCrypter::<ChaCha20Poly1305>::new(
Iv::from(iv),
&key,
ProtocolVersion::TLSv1_3,
)
.unwrap();
let mut payload = [0u8; <ChaCha20Poly1305 as BoringCipher>::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; <Aes128 as BoringCipher>::KEY_SIZE];
let iv = [0u8; 12];
let mut decrypter =
BoringAeadCrypter::<Aes128>::new(Iv::from(iv), &key, ProtocolVersion::TLSv1_2).unwrap();
let min_payload_len =
<Aes128 as BoringCipher>::EXPLICIT_NONCE_LEN + <Aes128 as BoringCipher>::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));
}
} }

View file

@ -20,6 +20,7 @@ impl BoringCipher for Aes128 {
const INTEGRITY_LIMIT: u64 = 1 << 52; const INTEGRITY_LIMIT: u64 = 1 << 52;
const CONFIDENTIALITY_LIMIT: u64 = 1 << 23; const CONFIDENTIALITY_LIMIT: u64 = 1 << 23;
const FIPS_APPROVED: bool = true;
fn new_cipher() -> Algorithm { fn new_cipher() -> Algorithm {
Algorithm::aes_128_gcm() Algorithm::aes_128_gcm()
@ -40,7 +41,7 @@ impl QuicCipher for Aes128 {
const KEY_SIZE: usize = <Self as BoringCipher>::KEY_SIZE; const KEY_SIZE: usize = <Self as BoringCipher>::KEY_SIZE;
const SAMPLE_LEN: usize = 16; 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::< quic_header_protection_mask::<
{ <Self as QuicCipher>::KEY_SIZE }, { <Self as QuicCipher>::KEY_SIZE },
{ <Self as QuicCipher>::SAMPLE_LEN }, { <Self as QuicCipher>::SAMPLE_LEN },
@ -65,6 +66,7 @@ impl BoringCipher for Aes256 {
const INTEGRITY_LIMIT: u64 = 1 << 52; const INTEGRITY_LIMIT: u64 = 1 << 52;
const CONFIDENTIALITY_LIMIT: u64 = 1 << 23; const CONFIDENTIALITY_LIMIT: u64 = 1 << 23;
const FIPS_APPROVED: bool = true;
fn new_cipher() -> Algorithm { fn new_cipher() -> Algorithm {
Algorithm::aes_256_gcm() Algorithm::aes_256_gcm()
@ -85,7 +87,7 @@ impl QuicCipher for Aes256 {
const KEY_SIZE: usize = <Self as BoringCipher>::KEY_SIZE; const KEY_SIZE: usize = <Self as BoringCipher>::KEY_SIZE;
const SAMPLE_LEN: usize = 16; 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::< quic_header_protection_mask::<
{ <Self as QuicCipher>::KEY_SIZE }, { <Self as QuicCipher>::KEY_SIZE },
{ <Self as QuicCipher>::SAMPLE_LEN }, { <Self as QuicCipher>::SAMPLE_LEN },
@ -97,19 +99,32 @@ fn quic_header_protection_mask<const KEY_SIZE: usize, const SAMPLE_LEN: usize>(
cipher: boring::symm::Cipher, cipher: boring::symm::Cipher,
hp_key: &[u8], hp_key: &[u8],
sample: &[u8], sample: &[u8],
) -> [u8; 5] { ) -> Result<[u8; 5], rustls::Error> {
assert!(hp_key.len() == KEY_SIZE); if hp_key.len() != KEY_SIZE {
assert!(sample.len() >= SAMPLE_LEN); 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 output = [0u8; SAMPLE_LEN];
let mut crypter = boring::symm::Crypter::new(cipher, boring::symm::Mode::Encrypt, hp_key, None) 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
let _ = len + crypter.finalize(&mut output[len..]).unwrap(); .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)] #[cfg(test)]

View file

@ -23,6 +23,7 @@ impl BoringCipher for ChaCha20Poly1305 {
const INTEGRITY_LIMIT: u64 = 1 << 36; const INTEGRITY_LIMIT: u64 = 1 << 36;
const CONFIDENTIALITY_LIMIT: u64 = u64::MAX; const CONFIDENTIALITY_LIMIT: u64 = u64::MAX;
const FIPS_APPROVED: bool = false;
fn new_cipher() -> Algorithm { fn new_cipher() -> Algorithm {
Algorithm::chacha20_poly1305() Algorithm::chacha20_poly1305()
@ -37,13 +38,21 @@ impl QuicCipher for ChaCha20Poly1305 {
const KEY_SIZE: usize = 32; const KEY_SIZE: usize = 32;
const SAMPLE_LEN: usize = 16; 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> {
assert!(hp_key.len() == <Self as QuicCipher>::KEY_SIZE); if hp_key.len() != <Self as QuicCipher>::KEY_SIZE {
assert!(sample.len() >= <Self as QuicCipher>::SAMPLE_LEN); return Err(rustls::Error::General(
"header protection key of invalid length".into(),
));
}
if sample.len() != <Self as QuicCipher>::SAMPLE_LEN {
return Err(rustls::Error::General("sample of invalid length".into()));
}
let mut mask = [0u8; 5]; 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. // 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. // RFC9001 5.4.4: The remaining 12 bytes are used as the nonce.
let nonce = &sample[4..16]; let nonce = &sample[4..16];
unsafe { unsafe {
@ -56,7 +65,7 @@ impl QuicCipher for ChaCha20Poly1305 {
counter, counter,
); );
}; };
mask Ok(mask)
} }
} }
@ -95,7 +104,8 @@ mod tests {
let sample = hex!("5e5cd55c41f69080575d7999c25a5bfb"); let sample = hex!("5e5cd55c41f69080575d7999c25a5bfb");
let hp_key = hex!("25a282b9e82f06f21f488917a4fc8f1b73573685608597d0efcb076b0ab7a7a4"); let hp_key = hex!("25a282b9e82f06f21f488917a4fc8f1b73573685608597d0efcb076b0ab7a7a4");
let expected_mask = hex!("aefefe7d03"); 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); assert_eq!(mask, expected_mask);
} }
} }

View file

@ -24,10 +24,14 @@ impl hash::Hash for Hash {
boring::nid::Nid::SHA256 => hash::HashAlgorithm::SHA256, boring::nid::Nid::SHA256 => hash::HashAlgorithm::SHA256,
boring::nid::Nid::SHA384 => hash::HashAlgorithm::SHA384, boring::nid::Nid::SHA384 => hash::HashAlgorithm::SHA384,
boring::nid::Nid::SHA512 => hash::HashAlgorithm::SHA512, 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 { fn output_len(&self) -> usize {
MessageDigest::from_nid(self.0) MessageDigest::from_nid(self.0)
.expect("failed getting digest") .expect("failed getting digest")

View file

@ -138,6 +138,10 @@ impl<T: BoringHash> RustlsHkdf for Hkdf<T> {
} }
rustls::crypto::hmac::Tag::new(&hash[..hash_len as usize]) rustls::crypto::hmac::Tag::new(&hash[..hash_len as usize])
} }
fn fips(&self) -> bool {
cfg!(feature = "fips")
}
} }
struct HkdfExpander { struct HkdfExpander {
@ -163,9 +167,17 @@ impl tls13::HkdfExpander for HkdfExpander {
info: &[&[u8]], info: &[&[u8]],
output: &mut [u8], output: &mut [u8],
) -> Result<(), tls13::OutputLengthError> { ) -> 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(); let info_concat = info.concat();
unsafe { unsafe {
boring_sys::HKDF_expand( cvt(boring_sys::HKDF_expand(
output.as_mut_ptr(), output.as_mut_ptr(),
output.len(), output.len(),
self.digest.as_ptr(), self.digest.as_ptr(),
@ -173,8 +185,9 @@ impl tls13::HkdfExpander for HkdfExpander {
self.prk_len, self.prk_len,
info_concat.as_ptr(), info_concat.as_ptr(),
info_concat.len(), info_concat.len(),
); ))
}; .map_err(|_| tls13::OutputLengthError)?;
}
Ok(()) Ok(())
} }
@ -201,3 +214,35 @@ impl tls13::HkdfExpander for HkdfExpander {
self.digest.size() 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::<Sha256>::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::<Sha256>::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));
}
}

View file

@ -20,16 +20,9 @@ struct BoringHmac(pub boring::nid::Nid);
impl crypto::hmac::Hmac for BoringHmac { impl crypto::hmac::Hmac for BoringHmac {
fn with_key(&self, key: &[u8]) -> Box<dyn crypto::hmac::Key> { fn with_key(&self, key: &[u8]) -> Box<dyn crypto::hmac::Key> {
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"); let md = MessageDigest::from_nid(self.0).expect("failed getting digest");
Box::new(BoringHmacKey { Box::new(BoringHmacKey {
ctx,
md, md,
key: Zeroizing::new(key.to_vec()), key: Zeroizing::new(key.to_vec()),
}) })
@ -40,33 +33,35 @@ impl crypto::hmac::Hmac for BoringHmac {
.expect("failed getting digest") .expect("failed getting digest")
.size() .size()
} }
fn fips(&self) -> bool {
cfg!(feature = "fips")
}
} }
struct BoringHmacKey { struct BoringHmacKey {
ctx: HmacCtx,
md: MessageDigest, md: MessageDigest,
key: Zeroizing<Vec<u8>>, key: Zeroizing<Vec<u8>>,
} }
impl BoringHmacKey { impl BoringHmacKey {
fn init(&self) { fn init(ctx: &HmacCtx, key: &[u8], md: MessageDigest) {
unsafe { unsafe {
// initialize a new hmac
cvt(boring_sys::HMAC_Init_ex( cvt(boring_sys::HMAC_Init_ex(
self.ctx.as_ptr(), ctx.as_ptr(),
self.key.as_ptr() as *const c_void, key.as_ptr() as *const c_void,
self.key.len(), key.len(),
self.md.as_ptr(), md.as_ptr(),
ptr::null_mut(), ptr::null_mut(),
)) ))
} }
.expect("failed initializing hmac"); .expect("failed initializing hmac");
} }
fn update(&self, bytes: &[u8]) { fn update(ctx: &HmacCtx, bytes: &[u8]) {
unsafe { unsafe {
cvt(boring_sys::HMAC_Update( cvt(boring_sys::HMAC_Update(
self.ctx.as_ptr(), ctx.as_ptr(),
bytes.as_ptr(), bytes.as_ptr(),
bytes.len(), bytes.len(),
)) ))
@ -74,11 +69,11 @@ impl BoringHmacKey {
.expect("failed updating hmac"); .expect("failed updating hmac");
} }
fn finish(&self, out: &mut [u8]) -> usize { fn finish(ctx: &HmacCtx, out: &mut [u8]) -> usize {
let mut out_len = 0; let mut out_len = 0;
unsafe { unsafe {
cvt(boring_sys::HMAC_Final( cvt(boring_sys::HMAC_Final(
self.ctx.as_ptr(), ctx.as_ptr(),
out.as_mut_ptr(), out.as_mut_ptr(),
&mut out_len, &mut out_len,
)) ))
@ -86,21 +81,28 @@ impl BoringHmacKey {
.expect("failed hmac final"); .expect("failed hmac final");
out_len as usize 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 { impl crypto::hmac::Key for BoringHmacKey {
fn sign_concat(&self, first: &[u8], middle: &[&[u8]], last: &[u8]) -> crypto::hmac::Tag { 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 { 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 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]) crypto::hmac::Tag::new(&out[..out_len])
} }

View file

@ -5,6 +5,7 @@ use boring::{
ec::{EcGroup, EcKey}, ec::{EcGroup, EcKey},
error::ErrorStack, error::ErrorStack,
nid::Nid, nid::Nid,
pkey::Id,
pkey::{PKey, PKeyRef, Private}, pkey::{PKey, PKeyRef, Private},
}; };
#[cfg(not(feature = "fips"))] #[cfg(not(feature = "fips"))]
@ -12,7 +13,6 @@ use boring_additions::evp::EvpPkeyCtx;
#[cfg(not(feature = "fips"))] #[cfg(not(feature = "fips"))]
use foreign_types::ForeignType; use foreign_types::ForeignType;
use rustls::crypto; use rustls::crypto;
use spki::der::Decode;
use crate::helper::log_and_map; use crate::helper::log_and_map;
#[cfg(not(feature = "fips"))] #[cfg(not(feature = "fips"))]
@ -97,18 +97,24 @@ impl KeyExchange {
/// Decodes a SPKI public key to it's raw public key component /// Decodes a SPKI public key to it's raw public key component
fn raw_public_key(pkey: &PKeyRef<Private>) -> Result<Vec<u8>, ErrorStack> { fn raw_public_key(pkey: &PKeyRef<Private>) -> Result<Vec<u8>, 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 return ec_key.public_key().to_bytes(
let pkey = spki::SubjectPublicKeyInfoRef::from_der(spki.as_ref()) ec_key.group(),
.expect("failed parsing spki bytes"); boring::ec::PointConversionForm::UNCOMPRESSED,
&mut bn_ctx,
);
}
// return the raw public key as a new vec let mut output = vec![0u8; pkey.raw_public_key_len()?];
Ok(Vec::from( let used_len = {
pkey.subject_public_key let used = pkey.raw_public_key(&mut output)?;
.as_bytes() used.len()
.expect("failed getting raw spki bytes"), };
)) output.truncate(used_len);
Ok(output)
} }
/// Derives a shared secret using the peer's raw public key /// Derives a shared secret using the peer's raw public key

View file

@ -32,6 +32,10 @@ impl crypto::SupportedKxGroup for X25519 {
fn name(&self) -> rustls::NamedGroup { fn name(&self) -> rustls::NamedGroup {
rustls::NamedGroup::X25519 rustls::NamedGroup::X25519
} }
fn fips(&self) -> bool {
false
}
} }
/// A secp256r1-based key exchange /// A secp256r1-based key exchange
@ -48,6 +52,10 @@ impl crypto::SupportedKxGroup for Secp256r1 {
fn name(&self) -> rustls::NamedGroup { fn name(&self) -> rustls::NamedGroup {
rustls::NamedGroup::secp256r1 rustls::NamedGroup::secp256r1
} }
fn fips(&self) -> bool {
cfg!(feature = "fips")
}
} }
/// A secp384r1-based key exchange /// A secp384r1-based key exchange
@ -64,4 +72,8 @@ impl crypto::SupportedKxGroup for Secp384r1 {
fn name(&self) -> rustls::NamedGroup { fn name(&self) -> rustls::NamedGroup {
rustls::NamedGroup::secp384r1 rustls::NamedGroup::secp384r1
} }
fn fips(&self) -> bool {
cfg!(feature = "fips")
}
} }

View file

@ -1,6 +1,8 @@
use std::sync::Arc; use std::sync::Arc;
use helper::log_and_map; use helper::log_and_map;
#[cfg(all(feature = "fips", feature = "log"))]
use log::warn;
use rustls::{ use rustls::{
crypto::{CryptoProvider, GetRandomFailed, SupportedKxGroup}, crypto::{CryptoProvider, GetRandomFailed, SupportedKxGroup},
SupportedCipherSuite, SupportedCipherSuite,
@ -33,6 +35,25 @@ pub fn provider() -> CryptoProvider {
} }
pub fn provider_with_ciphers(ciphers: Vec<rustls::SupportedCipherSuite>) -> CryptoProvider { pub fn provider_with_ciphers(ciphers: Vec<rustls::SupportedCipherSuite>) -> 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::<Vec<_>>();
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 { CryptoProvider {
cipher_suites: ciphers, cipher_suites: ciphers,
#[cfg(feature = "fips")] #[cfg(feature = "fips")]
@ -55,6 +76,10 @@ impl rustls::crypto::SecureRandom for Provider {
fn fill(&self, bytes: &mut [u8]) -> Result<(), rustls::crypto::GetRandomFailed> { 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)) 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 { impl rustls::crypto::KeyProvider for Provider {
@ -64,6 +89,10 @@ impl rustls::crypto::KeyProvider for Provider {
) -> Result<Arc<dyn rustls::sign::SigningKey>, rustls::Error> { ) -> Result<Arc<dyn rustls::sign::SigningKey>, rustls::Error> {
sign::BoringPrivateKey::try_from(key_der).map(|x| Arc::new(x) as _) sign::BoringPrivateKey::try_from(key_der).map(|x| Arc::new(x) as _)
} }
fn fips(&self) -> bool {
cfg!(feature = "fips")
}
} }
#[cfg(feature = "fips")] #[cfg(feature = "fips")]

View file

@ -29,6 +29,10 @@ impl crypto::tls12::Prf for PrfTls1WithDigest {
let digest = boring::hash::MessageDigest::from_nid(self.0).expect("failed getting digest"); let digest = boring::hash::MessageDigest::from_nid(self.0).expect("failed getting digest");
prf(digest, output, secret, label, seed).expect("failed calculating prf") prf(digest, output, secret, label, seed).expect("failed calculating prf")
} }
fn fips(&self) -> bool {
cfg!(feature = "fips")
}
} }
fn prf( fn prf(

View file

@ -2,6 +2,7 @@ use std::sync::Arc;
use boring::{ use boring::{
hash::MessageDigest, hash::MessageDigest,
nid::Nid,
pkey::{Id, PKeyRef, Private}, pkey::{Id, PKeyRef, Private},
rsa::Padding, rsa::Padding,
sign::{RsaPssSaltlen, Signer}, sign::{RsaPssSaltlen, Signer},
@ -20,16 +21,28 @@ const ALL_RSA_SCHEMES: &[SignatureScheme] = &[
SignatureScheme::RSA_PKCS1_SHA256, SignatureScheme::RSA_PKCS1_SHA256,
]; ];
const ALL_EC_SCHEMES: &[SignatureScheme] = &[ const EC_P256_SCHEMES: &[SignatureScheme] = &[SignatureScheme::ECDSA_NISTP256_SHA256];
SignatureScheme::ECDSA_NISTP256_SHA256, const EC_P384_SCHEMES: &[SignatureScheme] = &[SignatureScheme::ECDSA_NISTP384_SHA384];
SignatureScheme::ECDSA_NISTP384_SHA384, const EC_P521_SCHEMES: &[SignatureScheme] = &[SignatureScheme::ECDSA_NISTP521_SHA512];
SignatureScheme::ECDSA_NISTP521_SHA512,
];
/// An abstraction over a boringssl private key #[derive(Debug, Clone, Copy)]
/// used for signing 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)] #[derive(Debug)]
pub struct BoringPrivateKey(Arc<boring::pkey::PKey<Private>>, rustls::SignatureAlgorithm); pub struct BoringPrivateKey(Arc<boring::pkey::PKey<Private>>, KeyKind);
impl TryFrom<PrivateKeyDer<'static>> for BoringPrivateKey { impl TryFrom<PrivateKeyDer<'static>> for BoringPrivateKey {
type Error = rustls::Error; type Error = rustls::Error;
@ -37,25 +50,83 @@ impl TryFrom<PrivateKeyDer<'static>> for BoringPrivateKey {
fn try_from(value: PrivateKeyDer<'static>) -> Result<Self, Self::Error> { fn try_from(value: PrivateKeyDer<'static>) -> Result<Self, Self::Error> {
let pkey = match value { let pkey = match value {
PrivateKeyDer::Pkcs8(der) => { PrivateKeyDer::Pkcs8(der) => {
boring::pkey::PKey::private_key_from_pkcs8(der.secret_pkcs8_der()) boring::pkey::PKey::private_key_from_pkcs8(der.secret_pkcs8_der()).map_err(|e| {
.map_err(|e| log_and_map("private_key_from_pkcs8", e, ())) log_and_map(
"private_key_from_pkcs8",
e,
rustls::Error::General("failed loading private key".into()),
)
})
} }
PrivateKeyDer::Pkcs1(der) => { PrivateKeyDer::Pkcs1(der) => {
boring::pkey::PKey::private_key_from_der(der.secret_pkcs1_der()) boring::pkey::PKey::private_key_from_der(der.secret_pkcs1_der()).map_err(|e| {
.map_err(|e| log_and_map("private_key_from_der", e, ())) log_and_map(
"private_key_from_der_pkcs1",
e,
rustls::Error::General("failed loading private key".into()),
)
})
} }
_ => Err(()), 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()),
)
})
} }
.map_err(|_| rustls::Error::General("failed loading private key".into()))?; _ => {
return Err(rustls::Error::General(
"unsupported private key encoding".into(),
));
}
}?;
let sig = match pkey.id() { let kind = match pkey.id() {
Id::RSA => rustls::SignatureAlgorithm::RSA, Id::RSA => KeyKind::Rsa,
Id::EC => rustls::SignatureAlgorithm::ECDSA, Id::EC => {
Id::ED25519 => rustls::SignatureAlgorithm::ED25519, let ec_key = pkey.ec_key().map_err(|e| {
Id::ED448 => rustls::SignatureAlgorithm::ED448, 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())), _ => 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<Private>, key: &PKeyRef<Private>,
digest: MessageDigest, digest: MessageDigest,
padding: Padding, padding: Padding,
) -> Signer<'_> { ) -> Result<Signer<'_>, rustls::Error> {
let mut signer = Signer::new(digest, key).expect("failed getting signer"); let mut signer = Signer::new(digest, key).map_err(|e| {
signer log_and_map(
.set_rsa_padding(padding) "Signer::new",
.expect("failed setting padding"); 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 { if padding == Padding::PKCS1_PSS {
signer signer
.set_rsa_pss_saltlen(RsaPssSaltlen::DIGEST_LENGTH) .set_rsa_pss_saltlen(RsaPssSaltlen::DIGEST_LENGTH)
.expect("failed setting rsa_pss salt lengths"); .map_err(|e| {
signer log_and_map(
.set_rsa_mgf1_md(digest) "set_rsa_pss_saltlen",
.expect("failed setting mgf1 digest"); 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<Private>, digest: MessageDigest) -> Signer<'_> { fn ec_signer_from_params(
let signer = Signer::new(digest, key).expect("failed getting signer"); key: &PKeyRef<Private>,
signer digest: MessageDigest,
) -> Result<Signer<'_>, 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 { impl SigningKey for BoringPrivateKey {
fn choose_scheme( fn choose_scheme(
&self, &self,
offered: &[rustls::SignatureScheme], offered: &[rustls::SignatureScheme],
) -> Option<Box<dyn rustls::sign::Signer>> { ) -> Option<Box<dyn rustls::sign::Signer>> {
match self.1 { let scheme = match self.1 {
rustls::SignatureAlgorithm::RSA => ALL_RSA_SCHEMES KeyKind::Rsa => ALL_RSA_SCHEMES
.iter() .iter()
.find(|scheme| offered.contains(scheme)) .find(|scheme| offered.contains(scheme)),
.map(|&scheme| Box::new(BoringSigner(self.0.clone(), scheme)) as _), KeyKind::Ec(EcCurve::P256) => EC_P256_SCHEMES
rustls::SignatureAlgorithm::ECDSA => ALL_EC_SCHEMES
.iter() .iter()
.find(|scheme| offered.contains(scheme)) .find(|scheme| offered.contains(scheme)),
.map(|&scheme| Box::new(BoringSigner(self.0.clone(), scheme)) as _), KeyKind::Ec(EcCurve::P384) => EC_P384_SCHEMES
rustls::SignatureAlgorithm::ED25519 .iter()
if offered.contains(&rustls::SignatureScheme::ED25519) => .find(|scheme| offered.contains(scheme)),
{ KeyKind::Ec(EcCurve::P521) => EC_P521_SCHEMES
Some(Box::new(BoringSigner( .iter()
self.0.clone(), .find(|scheme| offered.contains(scheme)),
rustls::SignatureScheme::ED25519, KeyKind::Ed25519 if offered.contains(&rustls::SignatureScheme::ED25519) => {
))) Some(&rustls::SignatureScheme::ED25519)
} }
rustls::SignatureAlgorithm::ED448 KeyKind::Ed448 if offered.contains(&rustls::SignatureScheme::ED448) => {
if offered.contains(&rustls::SignatureScheme::ED448) => Some(&rustls::SignatureScheme::ED448)
{
Some(Box::new(BoringSigner(
self.0.clone(),
rustls::SignatureScheme::ED448,
)))
} }
_ => None, _ => None,
} }?;
Some(Box::new(BoringSigner(self.0.clone(), *scheme)))
} }
fn algorithm(&self) -> rustls::SignatureAlgorithm { 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)] #[derive(Debug)]
pub struct BoringSigner(Arc<boring::pkey::PKey<Private>>, rustls::SignatureScheme); pub struct BoringSigner(Arc<boring::pkey::PKey<Private>>, rustls::SignatureScheme);
impl BoringSigner { impl BoringSigner {
fn get_signer(&self) -> Signer<'_> { fn get_signer(&self) -> Result<Signer<'_>, rustls::Error> {
match self.1 { match self.1 {
SignatureScheme::RSA_PKCS1_SHA256 => { SignatureScheme::RSA_PKCS1_SHA256 => {
rsa_signer_from_params(self.0.as_ref(), MessageDigest::sha256(), Padding::PKCS1) rsa_signer_from_params(self.0.as_ref(), MessageDigest::sha256(), Padding::PKCS1)
@ -142,7 +243,6 @@ impl BoringSigner {
SignatureScheme::RSA_PKCS1_SHA512 => { SignatureScheme::RSA_PKCS1_SHA512 => {
rsa_signer_from_params(self.0.as_ref(), MessageDigest::sha512(), Padding::PKCS1) rsa_signer_from_params(self.0.as_ref(), MessageDigest::sha512(), Padding::PKCS1)
} }
SignatureScheme::RSA_PSS_SHA256 => { SignatureScheme::RSA_PSS_SHA256 => {
rsa_signer_from_params(self.0.as_ref(), MessageDigest::sha256(), Padding::PKCS1_PSS) rsa_signer_from_params(self.0.as_ref(), MessageDigest::sha256(), Padding::PKCS1_PSS)
} }
@ -152,7 +252,6 @@ impl BoringSigner {
SignatureScheme::RSA_PSS_SHA512 => { SignatureScheme::RSA_PSS_SHA512 => {
rsa_signer_from_params(self.0.as_ref(), MessageDigest::sha512(), Padding::PKCS1_PSS) rsa_signer_from_params(self.0.as_ref(), MessageDigest::sha512(), Padding::PKCS1_PSS)
} }
SignatureScheme::ECDSA_NISTP256_SHA256 => { SignatureScheme::ECDSA_NISTP256_SHA256 => {
ec_signer_from_params(self.0.as_ref(), MessageDigest::sha256()) ec_signer_from_params(self.0.as_ref(), MessageDigest::sha256())
} }
@ -162,19 +261,25 @@ impl BoringSigner {
SignatureScheme::ECDSA_NISTP521_SHA512 => { SignatureScheme::ECDSA_NISTP521_SHA512 => {
ec_signer_from_params(self.0.as_ref(), MessageDigest::sha512()) ec_signer_from_params(self.0.as_ref(), MessageDigest::sha512())
} }
SignatureScheme::ED25519 | SignatureScheme::ED448 => { 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()),
)
})
} }
_ => Err(rustls::Error::General(
_ => unimplemented!(), "unsupported signature scheme for private key".into(),
)),
} }
} }
} }
impl rustls::sign::Signer for BoringSigner { impl rustls::sign::Signer for BoringSigner {
fn sign(&self, message: &[u8]) -> Result<Vec<u8>, rustls::Error> { fn sign(&self, message: &[u8]) -> Result<Vec<u8>, rustls::Error> {
let mut signer = self.get_signer(); let mut signer = self.get_signer()?;
let max_sig_len = signer let max_sig_len = signer
.len() .len()
.map_err(|e| log_and_map("len", e, rustls::Error::General("failed signing".into())))?; .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 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<Private> {
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);
}
}

View file

@ -40,7 +40,7 @@ impl SignatureVerificationAlgorithm for BoringEcVerifier {
ec_verifier_from_params(public_key.as_ref(), MessageDigest::sha512()) 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))?; .map_err(|e| helper::log_and_map("ec_verifier_from_params", e, InvalidSignature))?;
@ -61,7 +61,7 @@ impl SignatureVerificationAlgorithm for BoringEcVerifier {
0x04, 0x00, 0x23, 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, 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( fn ec_verifier_from_params(
@ -92,7 +96,7 @@ fn group_for_scheme(scheme: SignatureScheme) -> Result<boring::ec::EcGroup, Erro
SignatureScheme::ECDSA_NISTP256_SHA256 => boring::nid::Nid::X9_62_PRIME256V1, SignatureScheme::ECDSA_NISTP256_SHA256 => boring::nid::Nid::X9_62_PRIME256V1,
SignatureScheme::ECDSA_NISTP384_SHA384 => boring::nid::Nid::SECP384R1, SignatureScheme::ECDSA_NISTP384_SHA384 => boring::nid::Nid::SECP384R1,
SignatureScheme::ECDSA_NISTP521_SHA512 => boring::nid::Nid::SECP521R1, SignatureScheme::ECDSA_NISTP521_SHA512 => boring::nid::Nid::SECP521R1,
_ => unimplemented!(), _ => unreachable!("BoringEcVerifier only supports configured ECDSA schemes"),
}; };
boring::ec::EcGroup::from_curve_name(nid) boring::ec::EcGroup::from_curve_name(nid)
} }

View file

@ -46,9 +46,13 @@ impl SignatureVerificationAlgorithm for BoringEdVerifier {
// rfc8410#section-3: 1.3.101.113: -> DER: 06 03 2B 65 71 // rfc8410#section-3: 1.3.101.113: -> DER: 06 03 2B 65 71
rustls_pki_types::AlgorithmIdentifier::from_slice(&[0x06, 0x03, 0x2B, 0x65, 0x71]) 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( fn ed_verifier_from_params(
@ -64,7 +68,7 @@ fn ed_public_key_for_scheme(
let nid = boring::nid::Nid::from_raw(match scheme { let nid = boring::nid::Nid::from_raw(match scheme {
SignatureScheme::ED25519 => boring_sys::EVP_PKEY_ED25519, SignatureScheme::ED25519 => boring_sys::EVP_PKEY_ED25519,
SignatureScheme::ED448 => boring_sys::EVP_PKEY_ED448, SignatureScheme::ED448 => boring_sys::EVP_PKEY_ED448,
_ => unimplemented!(), _ => return Err(ErrorStack::get()),
}); });
public_key(spki_spk, nid) public_key(spki_spk, nid)
} }

View file

@ -64,8 +64,8 @@ impl SignatureVerificationAlgorithm for BoringRsaVerifier {
Padding::PKCS1_PSS, Padding::PKCS1_PSS,
), ),
_ => unimplemented!(), _ => return Err(InvalidSignature),
}; }?;
verifier.verify_oneshot(signature, message).map_or_else( verifier.verify_oneshot(signature, message).map_or_else(
|_| Err(InvalidSignature), |_| Err(InvalidSignature),
|res| if res { Ok(()) } 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_SHA384 => alg_id::RSA_PSS_SHA384,
SignatureScheme::RSA_PSS_SHA512 => alg_id::RSA_PSS_SHA512, 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( fn rsa_verifier_from_params(
key: &boring::pkey::PKeyRef<boring::pkey::Public>, key: &boring::pkey::PKeyRef<boring::pkey::Public>,
digest: MessageDigest, digest: MessageDigest,
padding: Padding, padding: Padding,
) -> boring::sign::Verifier<'_> { ) -> Result<boring::sign::Verifier<'_>, InvalidSignature> {
let mut verifier = boring::sign::Verifier::new(digest, key).expect("failed getting verifier"); let mut verifier = boring::sign::Verifier::new(digest, key)
.map_err(|e| log_and_map("Verifier::new", e, InvalidSignature))?;
verifier verifier
.set_rsa_padding(padding) .set_rsa_padding(padding)
.expect("failed setting padding"); .map_err(|e| log_and_map("set_rsa_padding", e, InvalidSignature))?;
if padding == Padding::PKCS1_PSS { if padding == Padding::PKCS1_PSS {
verifier verifier
.set_rsa_pss_saltlen(RsaPssSaltlen::DIGEST_LENGTH) .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 verifier
.set_rsa_mgf1_md(digest) .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( pub(crate) fn decode_spki_spk(

View file

@ -5,8 +5,6 @@ use tokio::{
net::TcpStream, net::TcpStream,
}; };
#[cfg(feature = "tls12")]
use boring_rustls_provider::tls12;
use boring_rustls_provider::tls13; use boring_rustls_provider::tls13;
#[cfg(feature = "tls12")] #[cfg(feature = "tls12")]
use rustls::version::TLS12; use rustls::version::TLS12;
@ -15,6 +13,63 @@ use rustls_pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tokio_rustls::{TlsAcceptor, TlsConnector}; use tokio_rustls::{TlsAcceptor, TlsConnector};
fn tls13_provider_suites() -> Vec<SupportedCipherSuite> {
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<SupportedCipherSuite> {
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<SupportedCipherSuite> {
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] #[tokio::test]
async fn test_tls13_crypto() { async fn test_tls13_crypto() {
let pki = TestPki::new(&rcgen::PKCS_ECDSA_P256_SHA256); 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 root_store = pki.client_root_store();
let server_config = pki.server_config(); let server_config = pki.server_config();
#[cfg(feature = "fips")] let ciphers = tls13_provider_suites();
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),
];
for cipher in ciphers { for cipher in ciphers {
let config = ClientConfig::builder_with_provider(Arc::new( 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. /// Self-to-self TLS 1.3 handshake using only the X25519MLKEM768 PQ hybrid group.
#[cfg(feature = "mlkem")] #[cfg(feature = "mlkem")]
#[tokio::test] #[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::<Vec<_>>();
assert_eq!(suites, vec![CipherSuite::TLS13_AES_128_GCM_SHA256]);
}
#[test] #[test]
#[cfg(feature = "fips")] #[cfg(feature = "fips")]
fn fips_provider_restricts_kx_groups() { fn fips_provider_restricts_kx_groups() {
@ -310,6 +411,40 @@ fn non_fips_provider_keeps_non_fips_algorithms() {
.any(|group| group.name() == NamedGroup::X25519)); .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::<Vec<_>>();
assert_eq!(
suites,
vec![
CipherSuite::TLS13_CHACHA20_POLY1305_SHA256,
CipherSuite::TLS13_AES_128_GCM_SHA256,
]
);
}
#[test] #[test]
#[cfg(all(not(feature = "fips"), feature = "mlkem"))] #[cfg(all(not(feature = "fips"), feature = "mlkem"))]
fn non_fips_provider_includes_pq_group() { 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 root_store = pki.client_root_store();
let server_config = pki.server_config(); let server_config = pki.server_config();
#[cfg(feature = "fips")] let ciphers = tls12_provider_suites_for_ecdsa();
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),
];
for cipher in ciphers { for cipher in ciphers {
let config = ClientConfig::builder_with_provider(Arc::new( 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 root_store = pki.client_root_store();
let server_config = pki.server_config(); let server_config = pki.server_config();
#[cfg(feature = "fips")] let ciphers = tls12_provider_suites_for_rsa();
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),
];
for cipher in ciphers { for cipher in ciphers {
let config = ClientConfig::builder_with_provider(Arc::new( let config = ClientConfig::builder_with_provider(Arc::new(

View file

@ -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<Vec<PathBuf>, 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<PathBuf>) -> 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(())
}

View file

@ -2,7 +2,7 @@ use std::io::{stdout, Read, Write};
use std::net::TcpStream; use std::net::TcpStream;
use std::sync::Arc; use std::sync::Arc;
fn main() { fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init(); env_logger::init();
let mut root_store = rustls::RootCertStore::empty(); let mut root_store = rustls::RootCertStore::empty();
@ -11,13 +11,15 @@ fn main() {
let config = let config =
rustls::ClientConfig::builder_with_provider(boring_rustls_provider::provider().into()) rustls::ClientConfig::builder_with_provider(boring_rustls_provider::provider().into())
.with_safe_default_protocol_versions() .with_safe_default_protocol_versions()
.unwrap() .map_err(|_| std::io::Error::other("failed selecting protocol versions"))?
.with_root_certificates(root_store) .with_root_certificates(root_store)
.with_no_client_auth(); .with_no_client_auth();
let server_name = "www.rust-lang.org".try_into().unwrap(); let server_name = "www.rust-lang.org"
let mut conn = rustls::ClientConnection::new(Arc::new(config), server_name).unwrap(); .try_into()
let mut sock = TcpStream::connect("www.rust-lang.org:443").unwrap(); .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); let mut tls = rustls::Stream::new(&mut conn, &mut sock);
tls.write_all( tls.write_all(
concat!( concat!(
@ -28,16 +30,19 @@ fn main() {
"\r\n" "\r\n"
) )
.as_bytes(), .as_bytes(),
) )?;
.unwrap(); let ciphersuite = tls
let ciphersuite = tls.conn.negotiated_cipher_suite().unwrap(); .conn
.negotiated_cipher_suite()
.ok_or_else(|| std::io::Error::other("no negotiated ciphersuite"))?;
writeln!( writeln!(
&mut std::io::stderr(), &mut std::io::stderr(),
"Current ciphersuite: {:?}", "Current ciphersuite: {:?}",
ciphersuite.suite() ciphersuite.suite()
) )?;
.unwrap();
let mut plaintext = Vec::new(); let mut plaintext = Vec::new();
tls.read_to_end(&mut plaintext).unwrap(); tls.read_to_end(&mut plaintext)?;
stdout().write_all(&plaintext).unwrap(); stdout().write_all(&plaintext)?;
Ok(())
} }