Skip to content

Commit 5af8291

Browse files
rushilmehrakornelski
authored andcommitted
Expose client/server-side ECH
Resolves #282
1 parent 2561bdf commit 5af8291

File tree

11 files changed

+199
-0
lines changed

11 files changed

+199
-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: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ 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+
#[cfg(not(feature = "fips"))]
91+
use crate::ssl::ech::SslEchKeys;
9092
use crate::ssl::error::InnerError;
9193
use crate::stack::{Stack, StackRef, Stackable};
9294
use crate::x509::store::{X509Store, X509StoreBuilderRef, X509StoreRef};
@@ -110,6 +112,8 @@ mod async_callbacks;
110112
mod bio;
111113
mod callbacks;
112114
mod connector;
115+
#[cfg(not(feature = "fips"))]
116+
mod ech;
113117
mod error;
114118
mod mut_only;
115119
#[cfg(test)]
@@ -1956,6 +1960,16 @@ impl SslContextBuilder {
19561960
}
19571961
}
19581962

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

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

boring/src/ssl/test/ech.rs

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

boring/src/ssl/test/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ use super::CompliancePolicy;
2626

2727
mod cert_verify;
2828
mod custom_verify;
29+
#[cfg(not(feature = "fips"))]
30+
mod ech;
2931
mod private_key_method;
3032
mod server;
3133
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)