Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial ESNI/ECH client prototype #318

Closed
wants to merge 28 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
eee3245
Add the beginnings of ESNI
sayrer Oct 16, 2019
40355a5
checkpoint esni
sayrer Oct 20, 2019
7e43677
Almost working
sayrer Oct 26, 2019
ac39f27
Working handshake with only.esni.defo.ie
sayrer Oct 27, 2019
e9d5863
clean up some printing
sayrer Oct 27, 2019
524320d
Merge https://github.com/grafica/rustls
sayrer Oct 27, 2019
2a9373d
medium.com works
sayrer Oct 27, 2019
62d2e80
Use TLS1.3-only config
sayrer Oct 27, 2019
bc6e06c
Merge https://github.com/grafica/rustls
sayrer Oct 27, 2019
3ba1a21
Take out bogus list macro
sayrer Oct 28, 2019
96d1339
Account for mistakes in the ESNI drafts. See: https://t.co/Jp6wwa9DSX
sayrer Oct 28, 2019
ec84da0
Merge https://github.com/grafica/rustls
sayrer Oct 28, 2019
5bdfb38
Testing encrypt routing. problem must be in key calculation.
sayrer Nov 18, 2019
3793a7f
Fix plaintext, add more tests.
sayrer Nov 24, 2019
665a83a
Working client, still messy
sayrer Nov 24, 2019
cdf5142
Cleaned up esni.rs
sayrer Nov 24, 2019
72b96f8
Fix up
sayrer Nov 24, 2019
92adf2b
Cleaned up esni.rs
sayrer Nov 24, 2019
65a7910
Fix up
sayrer Nov 24, 2019
fba4f31
Merge https://github.com/grafica/rustls
sayrer Nov 24, 2019
416e590
Patch quic.rs, fix warnings.
sayrer Nov 24, 2019
312adbb
Fix QUIC, fix warnings
sayrer Nov 24, 2019
ea27074
Merge https://github.com/grafica/rustls
sayrer Nov 24, 2019
ed1af79
Formatting cleanup
sayrer Nov 25, 2019
65b0774
Remove some useless changes
sayrer Nov 25, 2019
feb8e40
Merge https://github.com/grafica/rustls
sayrer Nov 25, 2019
0920f90
Formatting cleanup
sayrer Nov 25, 2019
aa6317d
Merge https://github.com/grafica/rustls
sayrer Nov 25, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions rustls-mio/Cargo.toml
Expand Up @@ -22,6 +22,7 @@ log = { version = "0.4.4", optional = true }
rustls = { path = "../rustls" }
sct = "0.6"
webpki = "0.21.0"
trust-dns-resolver = { version = "0.12.0", features = ["dns-over-rustls", "dns-over-https-rustls"] }

[dev-dependencies]
ct-logs = "0.6"
Expand All @@ -47,6 +48,10 @@ path = "examples/tlsserver.rs"
name = "simpleclient"
path = "examples/simpleclient.rs"

[[example]]
name = "esniclient"
path = "examples/esniclient.rs"

[[example]]
name = "simple_0rtt_client"
path = "examples/simple_0rtt_client.rs"
Expand Down
104 changes: 104 additions & 0 deletions rustls-mio/examples/esniclient.rs
@@ -0,0 +1,104 @@
use std::sync::Arc;

use std::net::TcpStream;
use std::io::{Read, Write, stdout};
use std::iter::FromIterator;

use rustls;
use webpki;
use webpki_roots;
use rustls::Session;
use base64::decode;

extern crate trust_dns_resolver;
use trust_dns_resolver::config::*;
use trust_dns_resolver::Resolver;

fn main() {
let domain = "canbe.esni.defo.ie";
println!("\nContacting {:?} over ESNI\n", domain);

let dns_config = ResolverConfig::cloudflare_https();
let opts = ResolverOpts::default();
let addr = Address::new(domain);
let esni_bytes = resolve_esni(dns_config, opts, &addr);
let esni_hs = rustls::esni::create_esni_handshake(&esni_bytes).unwrap();

let mut config = rustls::esni::create_esni_config();
config.root_store.add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS);


let dns_name = webpki::DNSNameRef::try_from_ascii_str(domain).unwrap();
let mut sess = rustls::ClientSession::new_with_esni(&Arc::new(config), dns_name, esni_hs);
let mut sock = TcpStream::connect(domain.to_owned() + ":8443").unwrap();
let mut tls = rustls::Stream::new(&mut sess, &mut sock);
let host_header = format!("Host: {}\r\n", domain);
let mut headers = String::new();
headers.push_str("GET / HTTP/1.1\r\n");
headers.push_str(host_header.as_str());
headers.push_str("Connection: close\r\n");
headers.push_str("Accept-Encoding: identity\r\n");
headers.push_str("\r\n");
match tls.write(headers.as_bytes()) {
Ok(size) => {
println!("Received: {} bytes", size);
} ,
Err(e) => {
println!("Error: {:?}", e);
return;
}
}
let ciphersuite = tls.sess.get_negotiated_ciphersuite().unwrap();
writeln!(&mut std::io::stderr(), "\n\nNegotiated ciphersuite: {:?}", ciphersuite.suite).unwrap();
let mut plaintext = Vec::new();
match tls.read_to_end(&mut plaintext) {
Ok(success) => {
println!("read bytes: {}", success);
},
Err(e) => {
stdout().write_all(&plaintext).unwrap();

println!("failure to read the bytes: {:?}", e);

return;
}
}
stdout().write_all(&plaintext).unwrap();
}

pub fn resolve_esni(config: ResolverConfig, opts: ResolverOpts, address: &Address) -> Vec<u8> {
let resolver = Resolver::new(config, opts).unwrap();

let txt = resolver.txt_lookup(&address.esni_address()).unwrap();
let text = Vec::from_iter(txt.iter());
let mut bytes: Vec<u8> = Vec::new();
for txt_record in text.iter() {
for byte_slice in txt_record.txt_data().iter() {
for byte in byte_slice.iter() {
bytes.push(*byte);
}
}
}

decode(&bytes).unwrap()
}

pub struct Address {
domain: String
}

impl Address {
pub fn new(domain: &str) -> Address {
Address {
domain: String::from(domain)
}
}

pub fn esni_address(&self) -> String {
format!("_esni.{}.", self.domain)
}

pub fn dns_address(&self) -> String {
format!("{}.", self.domain)
}
}
1 change: 1 addition & 0 deletions rustls/Cargo.toml
Expand Up @@ -17,6 +17,7 @@ log = { version = "0.4.4", optional = true }
ring = "0.16.5"
sct = "0.6.0"
webpki = "0.21.0"
hex-literal = "0.2.1"

[features]
default = ["logging"]
Expand Down
1 change: 0 additions & 1 deletion rustls/src/cipher.rs
Expand Up @@ -251,7 +251,6 @@ impl Iv {
Self(value)
}

#[cfg(test)]
pub(crate) fn value(&self) -> &[u8; 12] { &self.0 }
}

Expand Down
7 changes: 6 additions & 1 deletion rustls/src/client/common.rs
Expand Up @@ -16,6 +16,7 @@ use crate::log::trace;
use webpki;

use std::mem;
use crate::esni::ESNIHandshakeData;

pub struct ServerCertDetails {
pub cert_chain: CertificatePayload,
Expand Down Expand Up @@ -60,11 +61,14 @@ pub struct HandshakeDetails {
pub session_id: SessionID,
pub sent_tls13_fake_ccs: bool,
pub dns_name: webpki::DNSName,
pub esni: Option<ESNIHandshakeData>,
pub extra_exts: Vec<ClientExtension>,
}

impl HandshakeDetails {
pub fn new(host_name: webpki::DNSName, extra_exts: Vec<ClientExtension>) -> HandshakeDetails {
pub fn new(host_name: webpki::DNSName,
esni: Option<ESNIHandshakeData>,
extra_exts: Vec<ClientExtension>) -> HandshakeDetails {
HandshakeDetails {
resuming_session: None,
transcript: hash_hs::HandshakeHash::new(),
Expand All @@ -74,6 +78,7 @@ impl HandshakeDetails {
session_id: SessionID::empty(),
sent_tls13_fake_ccs: false,
dns_name: host_name,
esni,
extra_exts,
}
}
Expand Down
45 changes: 36 additions & 9 deletions rustls/src/client/hs.rs
Expand Up @@ -10,7 +10,7 @@ use crate::msgs::handshake::{ProtocolNameList, ConvertProtocolNameList};
use crate::msgs::handshake::HelloRetryRequest;
use crate::msgs::handshake::{CertificateStatusRequest, SCTList};
use crate::msgs::enums::{PSKKeyExchangeMode, ECPointFormat};
use crate::msgs::codec::{Codec, Reader};
use crate::msgs::codec::{Codec, Reader, encode_vec_u16};
use crate::msgs::persist;
use crate::client::ClientSessionImpl;
use crate::session::SessionSecrets;
Expand All @@ -35,6 +35,7 @@ use crate::client::common::{ClientHelloDetails, ReceivedTicketDetails};
use crate::client::{tls12, tls13};

use webpki;
use crate::esni::ESNIHandshakeData;

macro_rules! extract_handshake(
( $m:expr, $t:path ) => (
Expand Down Expand Up @@ -132,9 +133,11 @@ struct InitialState {
}

impl InitialState {
fn new(host_name: webpki::DNSName, extra_exts: Vec<ClientExtension>) -> InitialState {
fn new(host_name: webpki::DNSName,
esni: Option<ESNIHandshakeData>,
extra_exts: Vec<ClientExtension>) -> InitialState {
InitialState {
handshake: HandshakeDetails::new(host_name, extra_exts),
handshake: HandshakeDetails::new(host_name, esni, extra_exts),
}
}

Expand All @@ -149,8 +152,9 @@ impl InitialState {


pub fn start_handshake(sess: &mut ClientSessionImpl, host_name: webpki::DNSName,
esni: Option<ESNIHandshakeData>,
extra_exts: Vec<ClientExtension>) -> NextState {
InitialState::new(host_name, extra_exts)
InitialState::new(host_name, esni, extra_exts)
.emit_initial_client_hello(sess)
}

Expand Down Expand Up @@ -215,9 +219,36 @@ fn emit_client_hello_for_retry(sess: &mut ClientSessionImpl,
if !supported_versions.is_empty() {
exts.push(ClientExtension::SupportedVersions(supported_versions));
}
if sess.config.enable_sni {

let keyshare_entries = if support_tls13 {
let ks = tls13::choose_kx_groups(sess, &mut hello, &mut handshake, retryreq);
let ret = ks.clone();
exts.push(ClientExtension::KeyShare(ks));
Some(ret)
} else {
None
};

if sess.config.enable_sni && sess.config.encrypt_sni {
if let Some(esni) = &handshake.esni {
if let Some(ks) = &keyshare_entries {
let mut ks_bytes= Vec::new();
encode_vec_u16(&mut ks_bytes, ks);
let esni_ext = ClientExtension::make_esni(handshake.dns_name.as_ref(), esni, ks_bytes, &handshake.randoms);
if let Some(ext) = esni_ext {
exts.push(ext);
}

// TODO: what if ESNI fails?
}
}

// TODO: what if ESNI is configured but there's no ESNI record?

} else if sess.config.enable_sni {
exts.push(ClientExtension::make_sni(handshake.dns_name.as_ref()));
}

exts.push(ClientExtension::ECPointFormats(ECPointFormatList::supported()));
exts.push(ClientExtension::NamedGroups(suites::KeyExchange::supported_groups().to_vec()));
exts.push(ClientExtension::SignatureAlgorithms(verify::supported_verify_schemes().to_vec()));
Expand All @@ -228,10 +259,6 @@ fn emit_client_hello_for_retry(sess: &mut ClientSessionImpl,
exts.push(ClientExtension::SignedCertificateTimestampRequest);
}

if support_tls13 {
tls13::choose_kx_groups(sess, &mut exts, &mut hello, &mut handshake, retryreq);
}

if let Some(cookie) = retryreq.and_then(HelloRetryRequest::get_cookie) {
exts.push(ClientExtension::Cookie(cookie.clone()));
}
Expand Down
24 changes: 21 additions & 3 deletions rustls/src/client/mod.rs
Expand Up @@ -13,6 +13,7 @@ use crate::anchors;
use crate::sign;
use crate::error::TLSError;
use crate::key;
use crate::esni::ESNIHandshakeData;
use crate::vecbuf::WriteV;
#[cfg(feature = "logging")]
use crate::log::trace;
Expand Down Expand Up @@ -124,6 +125,12 @@ pub struct ClientConfig {
/// The default is true.
pub enable_sni: bool,

/// If true, encrypt the SNI. Only used if `enable_sni` is true, in which case
/// the clear text SNI will not be sent.
///
/// The default is false.
pub encrypt_sni: bool,

/// How to verify the server certificate chain.
verifier: Arc<dyn verify::ServerCertVerifier>,

Expand Down Expand Up @@ -160,6 +167,7 @@ impl ClientConfig {
versions: vec![ProtocolVersion::TLSv1_3, ProtocolVersion::TLSv1_2],
ct_logs: None,
enable_sni: true,
encrypt_sni: false,
verifier: Arc::new(verify::WebPKIVerifier::new()),
key_log: Arc::new(NoKeyLog {}),
enable_early_data: false,
Expand Down Expand Up @@ -397,8 +405,8 @@ impl ClientSessionImpl {
}
}

pub fn start_handshake(&mut self, hostname: webpki::DNSName, extra_exts: Vec<ClientExtension>) {
self.state = Some(hs::start_handshake(self, hostname, extra_exts));
pub fn start_handshake(&mut self, hostname: webpki::DNSName, esni: Option<ESNIHandshakeData>, extra_exts: Vec<ClientExtension>) {
self.state = Some(hs::start_handshake(self, hostname, esni, extra_exts));
}

pub fn get_cipher_suites(&self) -> Vec<CipherSuite> {
Expand Down Expand Up @@ -596,7 +604,17 @@ impl ClientSession {
/// hostname of who we want to talk to.
pub fn new(config: &Arc<ClientConfig>, hostname: webpki::DNSNameRef) -> ClientSession {
let mut imp = ClientSessionImpl::new(config);
imp.start_handshake(hostname.into(), vec![]);
imp.start_handshake(hostname.into(), None,vec![]);
ClientSession { imp }
}

/// Make a new ClientSession. `config` controls how
/// we behave in the TLS protocol, `hostname` is the
/// hostname of who we want to talk to, and esni_keys are used to
/// encrypt the hostname in the ClientHello.
pub fn new_with_esni(config: &Arc<ClientConfig>, hostname: webpki::DNSNameRef, esni: ESNIHandshakeData) -> ClientSession {
let mut imp = ClientSessionImpl::new(config);
imp.start_handshake(hostname.into(), Some(esni), vec![]);
ClientSession { imp }
}

Expand Down
5 changes: 2 additions & 3 deletions rustls/src/client/tls13.rs
Expand Up @@ -82,10 +82,9 @@ fn save_kx_hint(sess: &mut ClientSessionImpl, dns_name: webpki::DNSNameRef, grou
}

pub fn choose_kx_groups(sess: &mut ClientSessionImpl,
exts: &mut Vec<ClientExtension>,
hello: &mut ClientHelloDetails,
handshake: &mut HandshakeDetails,
retryreq: Option<&HelloRetryRequest>) {
retryreq: Option<&HelloRetryRequest>) -> Vec<KeyShareEntry> {
// Choose our groups:
// - if we've been asked via HelloRetryRequest for a specific
// one, do that.
Expand Down Expand Up @@ -115,7 +114,7 @@ pub fn choose_kx_groups(sess: &mut ClientSessionImpl,
}
}

exts.push(ClientExtension::KeyShare(key_shares));
key_shares
}

/// This implements the horrifying TLS1.3 hack where PSK binders have a
Expand Down