Expose client/server-side ECH
Resolves https://github.com/cloudflare/boring/issues/282
This commit is contained in:
parent
2561bdf64d
commit
5af82912df
|
|
@ -128,6 +128,7 @@ 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;
|
||||
|
|
|
|||
|
|
@ -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<SslEchKeys, ErrorStack> {
|
||||
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(|_| ())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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};
|
||||
|
|
@ -110,6 +112,8 @@ mod async_callbacks;
|
|||
mod bio;
|
||||
mod callbacks;
|
||||
mod connector;
|
||||
#[cfg(not(feature = "fips"))]
|
||||
mod ech;
|
||||
mod error;
|
||||
mod mut_only;
|
||||
#[cfg(test)]
|
||||
|
|
@ -1956,6 +1960,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
|
||||
|
|
@ -3623,6 +3637,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.
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
§5D$lþ°SLb~Ê.<11>V<À.j¢ç¯:}´rˆ¶…
|
||||
Loading…
Reference in New Issue