Skip to content

Commit 4100155

Browse files
committed
Expose client/server-side ECH
Resolves #282
1 parent c2884be commit 4100155

File tree

11 files changed

+192
-0
lines changed

11 files changed

+192
-0
lines changed

boring/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ pub mod error;
128128
pub mod ex_data;
129129
pub mod fips;
130130
pub mod hash;
131+
#[cfg(not(feature = "fips"))]
131132
pub mod hpke;
132133
pub mod memcmp;
133134
pub mod nid;

boring/src/ssl/ech.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
use crate::ffi;
2+
use foreign_types::{ForeignType, ForeignTypeRef};
3+
use libc::c_int;
4+
5+
use crate::error::ErrorStack;
6+
use crate::hpke::HpkeKey;
7+
use crate::{cvt_0i, cvt_p};
8+
9+
foreign_type_and_impl_send_sync! {
10+
type CType = ffi::SSL_ECH_KEYS;
11+
fn drop = ffi::SSL_ECH_KEYS_free;
12+
13+
pub struct SslEchKeys;
14+
}
15+
16+
impl SslEchKeys {
17+
pub fn new() -> Result<SslEchKeys, ErrorStack> {
18+
unsafe {
19+
ffi::init();
20+
cvt_p(ffi::SSL_ECH_KEYS_new()).map(|p| SslEchKeys::from_ptr(p))
21+
}
22+
}
23+
}
24+
25+
impl SslEchKeysRef {
26+
pub fn add_key(
27+
&mut self,
28+
is_retry_config: bool,
29+
ech_config: &[u8],
30+
key: HpkeKey,
31+
) -> Result<(), ErrorStack> {
32+
unsafe {
33+
cvt_0i(ffi::SSL_ECH_KEYS_add(
34+
self.as_ptr(),
35+
is_retry_config as c_int,
36+
ech_config.as_ptr(),
37+
ech_config.len(),
38+
key.as_ptr(),
39+
))
40+
.map(|_| ())
41+
}
42+
}
43+
}

boring/src/ssl/mod.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ use crate::pkey::{HasPrivate, PKeyRef, Params, Private};
8787
use crate::srtp::{SrtpProtectionProfile, SrtpProtectionProfileRef};
8888
use crate::ssl::bio::BioMethod;
8989
use crate::ssl::callbacks::*;
90+
use crate::ssl::ech::SslEchKeys;
9091
use crate::ssl::error::InnerError;
9192
use crate::stack::{Stack, StackRef, Stackable};
9293
use crate::x509::store::{X509Store, X509StoreBuilderRef, X509StoreRef};
@@ -110,6 +111,8 @@ mod async_callbacks;
110111
mod bio;
111112
mod callbacks;
112113
mod connector;
114+
#[cfg(not(feature = "fips"))]
115+
mod ech;
113116
mod error;
114117
mod mut_only;
115118
#[cfg(test)]
@@ -1956,6 +1959,16 @@ impl SslContextBuilder {
19561959
}
19571960
}
19581961

1962+
/// Registers a list of ECH keys on the context. This list should contain new and old
1963+
/// ECHConfigs to allow stale DNS caches to update. Unlike most `SSL_CTX` APIs, this function
1964+
/// is safe to call even after the `SSL_CTX` has been associated with connections on various
1965+
/// threads.
1966+
#[cfg(not(feature = "fips"))]
1967+
#[corresponds(SSL_CTX_set1_ech_keys)]
1968+
pub fn set_ech_keys(&mut self, keys: SslEchKeys) -> Result<(), ErrorStack> {
1969+
unsafe { cvt(ffi::SSL_CTX_set1_ech_keys(self.as_ptr(), keys.as_ptr())).map(|_| ()) }
1970+
}
1971+
19591972
/// Consumes the builder, returning a new `SslContext`.
19601973
pub fn build(self) -> SslContext {
19611974
self.ctx
@@ -3623,6 +3636,77 @@ impl SslRef {
36233636
pub fn add_chain_cert(&mut self, cert: &X509Ref) -> Result<(), ErrorStack> {
36243637
unsafe { cvt(ffi::SSL_add1_chain_cert(self.as_ptr(), cert.as_ptr())).map(|_| ()) }
36253638
}
3639+
3640+
/// Configures `ech_config_list` on `SSL` for offering ECH during handshakes. If the server
3641+
/// cannot decrypt the encrypted ClientHello, `SSL` will instead handshake using
3642+
/// the cleartext parameters of the ClientHelloOuter.
3643+
///
3644+
/// Clients should use `get_ech_name_override` to verify the server certificate in case of ECH
3645+
/// rejection, and follow up with `get_ech_retry_configs` to retry the connection with a fresh
3646+
/// set of ECHConfigs. If the retry also fails, clients should report a connection failure.
3647+
#[cfg(not(feature = "fips"))]
3648+
#[corresponds(SSL_set1_ech_config_list)]
3649+
pub fn set_ech_config_list(&mut self, ech_config_list: &[u8]) -> Result<(), ErrorStack> {
3650+
unsafe {
3651+
cvt_0i(ffi::SSL_set1_ech_config_list(
3652+
self.as_ptr(),
3653+
ech_config_list.as_ptr(),
3654+
ech_config_list.len(),
3655+
))
3656+
.map(|_| ())
3657+
}
3658+
}
3659+
3660+
/// This function returns a serialized `ECHConfigList` as provided by the
3661+
/// server, if one exists.
3662+
///
3663+
/// Clients should call this function when handling an `SSL_R_ECH_REJECTED` error code to
3664+
/// recover from potential key mismatches. If the result is `Some`, the client should retry the
3665+
/// connection using the returned `ECHConfigList`.
3666+
#[cfg(not(feature = "fips"))]
3667+
#[corresponds(SSL_get0_ech_retry_configs)]
3668+
pub fn get_ech_retry_configs(&self) -> Option<&[u8]> {
3669+
unsafe {
3670+
let mut data = ptr::null();
3671+
let mut len: usize = 0;
3672+
ffi::SSL_get0_ech_retry_configs(self.as_ptr(), &mut data, &mut len);
3673+
3674+
if data.is_null() {
3675+
None
3676+
} else {
3677+
Some(slice::from_raw_parts(data, len))
3678+
}
3679+
}
3680+
}
3681+
3682+
/// If `SSL` is a client and the server rejects ECH, this function returns the public name
3683+
/// associated with the ECHConfig that was used to attempt ECH.
3684+
///
3685+
/// Clients should call this function during the certificate verification callback to
3686+
/// ensure the server's certificate is valid for the public name, which is required to
3687+
/// authenticate retry configs.
3688+
#[cfg(not(feature = "fips"))]
3689+
#[corresponds(SSL_get0_ech_name_override)]
3690+
pub fn get_ech_name_override(&self) -> Option<&[u8]> {
3691+
unsafe {
3692+
let mut data: *const c_char = ptr::null();
3693+
let mut len: usize = 0;
3694+
ffi::SSL_get0_ech_name_override(self.as_ptr(), &mut data, &mut len);
3695+
3696+
if data.is_null() {
3697+
None
3698+
} else {
3699+
Some(slice::from_raw_parts(data as *const u8, len))
3700+
}
3701+
}
3702+
}
3703+
3704+
// Whether or not `SSL` negotiated ECH.
3705+
#[cfg(not(feature = "fips"))]
3706+
#[corresponds(SSL_ech_accepted)]
3707+
pub fn ech_accepted(&self) -> bool {
3708+
unsafe { ffi::SSL_ech_accepted(self.as_ptr()) != 0 }
3709+
}
36263710
}
36273711

36283712
/// An SSL stream midway through the handshake process.

boring/src/ssl/test/ech.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
use crate::hpke::HpkeKey;
2+
use crate::ssl::ech::SslEchKeys;
3+
use crate::ssl::test::Server;
4+
use crate::ssl::HandshakeError;
5+
6+
static ECH_CONFIG_LIST: &[u8] = include_bytes!("../../../test/echconfiglist");
7+
static ECH_CONFIG: &[u8] = include_bytes!("../../../test/echconfig");
8+
static ECH_KEY: &[u8] = include_bytes!("../../../test/echkey");
9+
10+
static ECH_CONFIG_2: &[u8] = include_bytes!("../../../test/echconfig-2");
11+
static ECH_KEY_2: &[u8] = include_bytes!("../../../test/echkey-2");
12+
13+
#[test]
14+
fn ech() {
15+
let server = {
16+
let key = HpkeKey::dhkem_p256_sha256(ECH_KEY).unwrap();
17+
let mut ech_keys = SslEchKeys::new().unwrap();
18+
ech_keys.add_key(true, ECH_CONFIG, key).unwrap();
19+
20+
let mut builder = Server::builder();
21+
builder.ctx().set_ech_keys(ech_keys).unwrap();
22+
23+
builder.build()
24+
};
25+
26+
let mut client = server.client_with_root_ca().build().builder();
27+
client.ssl().set_ech_config_list(ECH_CONFIG_LIST).unwrap();
28+
client.ssl().set_hostname("foobar.com").unwrap();
29+
30+
let ssl_stream = client.connect();
31+
assert!(ssl_stream.ssl().ech_accepted())
32+
}
33+
34+
#[test]
35+
fn ech_rejection() {
36+
let server = {
37+
let key = HpkeKey::dhkem_p256_sha256(ECH_KEY_2).unwrap();
38+
let mut ech_keys = SslEchKeys::new().unwrap();
39+
ech_keys.add_key(true, ECH_CONFIG_2, key).unwrap();
40+
41+
let mut builder = Server::builder();
42+
builder.ctx().set_ech_keys(ech_keys).unwrap();
43+
44+
builder.build()
45+
};
46+
47+
let mut client = server.client_with_root_ca().build().builder();
48+
// Server is initialized using `ECH_CONFIG_2`, so using `ECH_CONFIG_LIST` instead of
49+
// `ECH_CONFIG_LIST_2` should trigger rejection.
50+
client.ssl().set_ech_config_list(ECH_CONFIG_LIST).unwrap();
51+
client.ssl().set_hostname("foobar.com").unwrap();
52+
let HandshakeError::Failure(failed_ssl_stream) = client.connect_err() else {
53+
panic!("wrong HandshakeError failure variant!");
54+
};
55+
56+
assert_eq!(
57+
failed_ssl_stream.ssl().get_ech_name_override(),
58+
Some(b"ech.com".to_vec().as_ref())
59+
);
60+
assert!(failed_ssl_stream.ssl().get_ech_retry_configs().is_some());
61+
assert!(!failed_ssl_stream.ssl().ech_accepted())
62+
}

boring/src/ssl/test/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use super::CompliancePolicy;
2626

2727
mod cert_verify;
2828
mod custom_verify;
29+
mod ech;
2930
mod private_key_method;
3031
mod server;
3132
mod session;

boring/test/echconfig

62 Bytes
Binary file not shown.

boring/test/echconfig-2

62 Bytes
Binary file not shown.

boring/test/echconfiglist

64 Bytes
Binary file not shown.

boring/test/echconfiglist-2

64 Bytes
Binary file not shown.

boring/test/echkey

32 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)