diff --git a/boring/src/hpke.rs b/boring/src/hpke.rs new file mode 100644 index 00000000..7f788224 --- /dev/null +++ b/boring/src/hpke.rs @@ -0,0 +1,31 @@ +use crate::error::ErrorStack; +use crate::{cvt_0i, cvt_p, ffi}; + +use foreign_types::ForeignType; + +foreign_type_and_impl_send_sync! { + type CType = ffi::EVP_HPKE_KEY; + fn drop = ffi::EVP_HPKE_KEY_free; + + pub struct HpkeKey; +} + +impl HpkeKey { + /// Allocates and initializes a key with the `EVP_HPKE_KEY` type using the + /// `EVP_hpke_x25519_hkdf_sha256` KEM algorithm. + pub fn dhkem_p256_sha256(pkey: &[u8]) -> Result { + unsafe { + ffi::init(); + let hpke = cvt_p(ffi::EVP_HPKE_KEY_new()).map(|p| HpkeKey::from_ptr(p))?; + + cvt_0i(ffi::EVP_HPKE_KEY_init( + hpke.as_ptr(), + ffi::EVP_hpke_x25519_hkdf_sha256(), + pkey.as_ptr(), + pkey.len(), + ))?; + + Ok(hpke) + } + } +} diff --git a/boring/src/lib.rs b/boring/src/lib.rs index 85ee501f..e471d861 100644 --- a/boring/src/lib.rs +++ b/boring/src/lib.rs @@ -123,6 +123,8 @@ pub mod error; pub mod ex_data; pub mod fips; pub mod hash; +#[cfg(not(feature = "fips"))] +pub mod hpke; pub mod memcmp; pub mod nid; pub mod pkcs12; diff --git a/boring/src/ssl/ech.rs b/boring/src/ssl/ech.rs new file mode 100644 index 00000000..d7b49328 --- /dev/null +++ b/boring/src/ssl/ech.rs @@ -0,0 +1,43 @@ +use crate::ffi; +use foreign_types::{ForeignType, ForeignTypeRef}; +use libc::c_int; + +use crate::error::ErrorStack; +use crate::hpke::HpkeKey; +use crate::{cvt_0i, cvt_p}; + +foreign_type_and_impl_send_sync! { + type CType = ffi::SSL_ECH_KEYS; + fn drop = ffi::SSL_ECH_KEYS_free; + + pub struct SslEchKeys; +} + +impl SslEchKeys { + pub fn new() -> Result { + unsafe { + ffi::init(); + cvt_p(ffi::SSL_ECH_KEYS_new()).map(|p| SslEchKeys::from_ptr(p)) + } + } +} + +impl SslEchKeysRef { + pub fn add_key( + &mut self, + is_retry_config: bool, + ech_config: &[u8], + key: HpkeKey, + ) -> Result<(), ErrorStack> { + unsafe { + cvt_0i(ffi::SSL_ECH_KEYS_add( + self.as_ptr(), + is_retry_config as c_int, + ech_config.as_ptr(), + ech_config.len(), + key.as_ptr(), + )) + .map(|_| ()) + } + } +} diff --git a/boring/src/ssl/mod.rs b/boring/src/ssl/mod.rs index e99c6f21..c0216525 100644 --- a/boring/src/ssl/mod.rs +++ b/boring/src/ssl/mod.rs @@ -87,6 +87,8 @@ use crate::pkey::{HasPrivate, PKeyRef, Params, Private}; use crate::srtp::{SrtpProtectionProfile, SrtpProtectionProfileRef}; use crate::ssl::bio::BioMethod; use crate::ssl::callbacks::*; +#[cfg(not(feature = "fips"))] +use crate::ssl::ech::SslEchKeys; use crate::ssl::error::InnerError; use crate::stack::{Stack, StackRef, Stackable}; use crate::x509::store::{X509Store, X509StoreBuilderRef, X509StoreRef}; @@ -114,6 +116,8 @@ mod callbacks; #[cfg(feature = "cert-compression")] mod cert_compression; mod connector; +#[cfg(not(feature = "fips"))] +mod ech; mod error; mod mut_only; #[cfg(test)] @@ -2014,6 +2018,16 @@ impl SslContextBuilder { } } + /// Registers a list of ECH keys on the context. This list should contain new and old + /// ECHConfigs to allow stale DNS caches to update. Unlike most `SSL_CTX` APIs, this function + /// is safe to call even after the `SSL_CTX` has been associated with connections on various + /// threads. + #[cfg(not(feature = "fips"))] + #[corresponds(SSL_CTX_set1_ech_keys)] + pub fn set_ech_keys(&mut self, keys: SslEchKeys) -> Result<(), ErrorStack> { + unsafe { cvt(ffi::SSL_CTX_set1_ech_keys(self.as_ptr(), keys.as_ptr())).map(|_| ()) } + } + /// Consumes the builder, returning a new `SslContext`. pub fn build(self) -> SslContext { self.ctx @@ -3573,6 +3587,77 @@ impl SslRef { pub fn add_chain_cert(&mut self, cert: &X509Ref) -> Result<(), ErrorStack> { unsafe { cvt(ffi::SSL_add1_chain_cert(self.as_ptr(), cert.as_ptr())).map(|_| ()) } } + + /// Configures `ech_config_list` on `SSL` for offering ECH during handshakes. If the server + /// cannot decrypt the encrypted ClientHello, `SSL` will instead handshake using + /// the cleartext parameters of the ClientHelloOuter. + /// + /// Clients should use `get_ech_name_override` to verify the server certificate in case of ECH + /// rejection, and follow up with `get_ech_retry_configs` to retry the connection with a fresh + /// set of ECHConfigs. If the retry also fails, clients should report a connection failure. + #[cfg(not(feature = "fips"))] + #[corresponds(SSL_set1_ech_config_list)] + pub fn set_ech_config_list(&mut self, ech_config_list: &[u8]) -> Result<(), ErrorStack> { + unsafe { + cvt_0i(ffi::SSL_set1_ech_config_list( + self.as_ptr(), + ech_config_list.as_ptr(), + ech_config_list.len(), + )) + .map(|_| ()) + } + } + + /// This function returns a serialized `ECHConfigList` as provided by the + /// server, if one exists. + /// + /// Clients should call this function when handling an `SSL_R_ECH_REJECTED` error code to + /// recover from potential key mismatches. If the result is `Some`, the client should retry the + /// connection using the returned `ECHConfigList`. + #[cfg(not(feature = "fips"))] + #[corresponds(SSL_get0_ech_retry_configs)] + pub fn get_ech_retry_configs(&self) -> Option<&[u8]> { + unsafe { + let mut data = ptr::null(); + let mut len: usize = 0; + ffi::SSL_get0_ech_retry_configs(self.as_ptr(), &mut data, &mut len); + + if data.is_null() { + None + } else { + Some(slice::from_raw_parts(data, len)) + } + } + } + + /// If `SSL` is a client and the server rejects ECH, this function returns the public name + /// associated with the ECHConfig that was used to attempt ECH. + /// + /// Clients should call this function during the certificate verification callback to + /// ensure the server's certificate is valid for the public name, which is required to + /// authenticate retry configs. + #[cfg(not(feature = "fips"))] + #[corresponds(SSL_get0_ech_name_override)] + pub fn get_ech_name_override(&self) -> Option<&[u8]> { + unsafe { + let mut data: *const c_char = ptr::null(); + let mut len: usize = 0; + ffi::SSL_get0_ech_name_override(self.as_ptr(), &mut data, &mut len); + + if data.is_null() { + None + } else { + Some(slice::from_raw_parts(data as *const u8, len)) + } + } + } + + // Whether or not `SSL` negotiated ECH. + #[cfg(not(feature = "fips"))] + #[corresponds(SSL_ech_accepted)] + pub fn ech_accepted(&self) -> bool { + unsafe { ffi::SSL_ech_accepted(self.as_ptr()) != 0 } + } } /// An SSL stream midway through the handshake process. diff --git a/boring/src/ssl/test/ech.rs b/boring/src/ssl/test/ech.rs new file mode 100644 index 00000000..7413240e --- /dev/null +++ b/boring/src/ssl/test/ech.rs @@ -0,0 +1,67 @@ +use crate::hpke::HpkeKey; +use crate::ssl::ech::SslEchKeys; +use crate::ssl::test::Server; +use crate::ssl::HandshakeError; + +// For future reference, these configs are generated by building the bssl tool (the binary is built +// alongside boringssl) and running the following command: +// +// ./bssl generate-ech -out-ech-config-list ./list -out-ech-config ./config -out-private-key ./key +// -public-name ech.com -config-id 1 +static ECH_CONFIG_LIST: &[u8] = include_bytes!("../../../test/echconfiglist"); +static ECH_CONFIG: &[u8] = include_bytes!("../../../test/echconfig"); +static ECH_KEY: &[u8] = include_bytes!("../../../test/echkey"); + +static ECH_CONFIG_2: &[u8] = include_bytes!("../../../test/echconfig-2"); +static ECH_KEY_2: &[u8] = include_bytes!("../../../test/echkey-2"); + +#[test] +fn ech() { + let server = { + let key = HpkeKey::dhkem_p256_sha256(ECH_KEY).unwrap(); + let mut ech_keys = SslEchKeys::new().unwrap(); + ech_keys.add_key(true, ECH_CONFIG, key).unwrap(); + + let mut builder = Server::builder(); + builder.ctx().set_ech_keys(ech_keys).unwrap(); + + builder.build() + }; + + let mut client = server.client_with_root_ca().build().builder(); + client.ssl().set_ech_config_list(ECH_CONFIG_LIST).unwrap(); + client.ssl().set_hostname("foobar.com").unwrap(); + + let ssl_stream = client.connect(); + assert!(ssl_stream.ssl().ech_accepted()) +} + +#[test] +fn ech_rejection() { + let server = { + let key = HpkeKey::dhkem_p256_sha256(ECH_KEY_2).unwrap(); + let mut ech_keys = SslEchKeys::new().unwrap(); + ech_keys.add_key(true, ECH_CONFIG_2, key).unwrap(); + + let mut builder = Server::builder(); + builder.ctx().set_ech_keys(ech_keys).unwrap(); + + builder.build() + }; + + let mut client = server.client_with_root_ca().build().builder(); + // Server is initialized using `ECH_CONFIG_2`, so using `ECH_CONFIG_LIST` instead of + // `ECH_CONFIG_LIST_2` should trigger rejection. + client.ssl().set_ech_config_list(ECH_CONFIG_LIST).unwrap(); + client.ssl().set_hostname("foobar.com").unwrap(); + let HandshakeError::Failure(failed_ssl_stream) = client.connect_err() else { + panic!("wrong HandshakeError failure variant!"); + }; + + assert_eq!( + failed_ssl_stream.ssl().get_ech_name_override(), + Some(b"ech.com".to_vec().as_ref()) + ); + assert!(failed_ssl_stream.ssl().get_ech_retry_configs().is_some()); + assert!(!failed_ssl_stream.ssl().ech_accepted()) +} diff --git a/boring/src/ssl/test/mod.rs b/boring/src/ssl/test/mod.rs index 6010cf98..ab11780e 100644 --- a/boring/src/ssl/test/mod.rs +++ b/boring/src/ssl/test/mod.rs @@ -26,6 +26,8 @@ use super::CompliancePolicy; mod cert_verify; mod custom_verify; +#[cfg(not(feature = "fips"))] +mod ech; mod private_key_method; mod server; mod session; diff --git a/boring/test/echconfig b/boring/test/echconfig new file mode 100644 index 00000000..cd0649ce Binary files /dev/null and b/boring/test/echconfig differ diff --git a/boring/test/echconfig-2 b/boring/test/echconfig-2 new file mode 100644 index 00000000..b6e4d005 Binary files /dev/null and b/boring/test/echconfig-2 differ diff --git a/boring/test/echconfiglist b/boring/test/echconfiglist new file mode 100644 index 00000000..c055475e Binary files /dev/null and b/boring/test/echconfiglist differ diff --git a/boring/test/echconfiglist-2 b/boring/test/echconfiglist-2 new file mode 100644 index 00000000..58f0f224 Binary files /dev/null and b/boring/test/echconfiglist-2 differ diff --git a/boring/test/echkey b/boring/test/echkey new file mode 100644 index 00000000..35301feb Binary files /dev/null and b/boring/test/echkey differ diff --git a/boring/test/echkey-2 b/boring/test/echkey-2 new file mode 100644 index 00000000..3222ee5e --- /dev/null +++ b/boring/test/echkey-2 @@ -0,0 +1 @@ +§5D$lþ°SLb~Ê.V<À.j¢ç¯:}´rˆ¶… \ No newline at end of file