Skip to content

Commit e1bbaab

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

File tree

10 files changed

+185
-0
lines changed

10 files changed

+185
-0
lines changed

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: 78 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,7 @@ mod async_callbacks;
110111
mod bio;
111112
mod callbacks;
112113
mod connector;
114+
mod ech;
113115
mod error;
114116
mod mut_only;
115117
#[cfg(test)]
@@ -1956,6 +1958,15 @@ impl SslContextBuilder {
19561958
}
19571959
}
19581960

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

36283706
/// 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.

boring/test/echkey-2

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
�5D$l��SLb~�.�V<�.j��:}�r���

0 commit comments

Comments
 (0)