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

Rustls 0.20 and HttpsConnectorBuilder #156

Merged
merged 6 commits into from
Nov 15, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,33 +11,32 @@ repository = "https://github.com/ctz/hyper-rustls"

[dependencies]
log = "0.4.4"
ct-logs = { version = "^0.8", optional = true }
hyper = { version = "0.14", default-features = false, features = ["client", "http1"] }
rustls = "0.19"
rustls-native-certs = { version = "0.5.0", optional = true }
rustls = "0.20"
rustls-native-certs = { version = "0.6", optional = true }
tokio = "1.0"
tokio-rustls = "0.22"
webpki = "0.21.0"
webpki-roots = { version = "0.21", optional = true }
tokio-rustls = "0.23"
webpki-roots = { version = "0.22", optional = true }

[dev-dependencies]
async-stream = "0.3.0"
tokio = { version = "1.0", features = ["io-std", "macros", "net", "rt-multi-thread"] }
hyper = { version = "0.14", features = ["full"] }
futures-util = { version = "0.3.1", default-features = false }
rustls-pemfile = "0.2.1"

[features]
default = ["native-tokio", "http1"]
http1 = ["hyper/http1"]
http2 = ["hyper/http2"]
webpki-tokio = ["tokio-runtime", "webpki-roots"]
native-tokio = ["tokio-runtime", "rustls-native-certs"]
tokio-runtime = ["hyper/runtime", "ct-logs"]
tokio-runtime = ["hyper/runtime"]

[[example]]
name = "client"
path = "examples/client.rs"
required-features = ["native-tokio", "tokio-runtime"]
required-features = ["native-tokio", "http1"]

[[example]]
name = "server"
Expand Down
37 changes: 24 additions & 13 deletions examples/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
//! First parameter is the mandatory URL to GET.
//! Second parameter is an optional path to CA store.
use hyper::{body::to_bytes, client, Body, Uri};
use hyper_rustls::ConfigBuilderExt;
use rustls::RootCertStore;

use std::str::FromStr;
use std::{env, fs, io};

Expand Down Expand Up @@ -40,24 +43,32 @@ async fn run_client() -> io::Result<()> {
None => None,
};

// Prepare the HTTPS connector.
let https = match ca {
// Prepare the TLS client config
let tls = match ca {
Some(ref mut rd) => {
// Build an HTTP connector which supports HTTPS too.
let mut http = client::HttpConnector::new();
http.enforce_http(false);
// Build a TLS client, using the custom CA store for lookups.
let mut tls = rustls::ClientConfig::new();
tls.root_store
.add_pem_file(rd)
// Read trust roots
let certs = rustls_pemfile::certs(rd)
.map_err(|_| error("failed to load custom CA store".into()))?;
// Join the above part into an HTTPS connector.
hyper_rustls::HttpsConnector::from((http, tls))
let mut roots = RootCertStore::empty();
roots.add_parsable_certificates(&certs);
// Build a TLS client, using the custom CA store for lookups.
rustls::ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(roots)
.with_no_client_auth()
}
// Default HTTPS connector.
None => hyper_rustls::HttpsConnector::with_native_roots(),
None => rustls::ClientConfig::builder()
.with_safe_defaults()
.with_native_roots(),
};

// Build an HTTP connector which supports HTTPS too.
let mut http = client::HttpConnector::new();
http.enforce_http(false);

// Join the above parts into an HTTPS connector.
let https = hyper_rustls::HttpsConnector::from((http, tls));

// Build the hyper client from the HTTPS connector.
let client: client::Client<_, hyper::Body> = client::Client::builder().build(https);

Expand Down
23 changes: 13 additions & 10 deletions examples/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@
//! Certificate and private key are hardcoded to sample files.
//! hyper will automatically use HTTP/2 if a client starts talking HTTP/2,
//! otherwise HTTP/1.1 will be used.
use std::{env, fs, io, sync};

use async_stream::stream;
use futures_util::future::TryFutureExt;
use hyper::server::accept;
use hyper::service::{make_service_fn, service_fn};
use hyper::{Body, Method, Request, Response, Server, StatusCode};
use rustls::internal::pemfile;
use std::vec::Vec;
use std::{env, fs, io, sync};
use tokio::net::TcpListener;
use tokio_rustls::TlsAcceptor;

Expand Down Expand Up @@ -43,12 +42,13 @@ async fn run_server() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Load private key.
let key = load_private_key("examples/sample.rsa")?;
// Do not use client certificate authentication.
let mut cfg = rustls::ServerConfig::new(rustls::NoClientAuth::new());
// Select a certificate to use.
cfg.set_single_cert(certs, key)
let mut cfg = rustls::ServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth()
.with_single_cert(certs, key)
.map_err(|e| error(format!("{}", e)))?;
// Configure ALPN to accept HTTP/2, HTTP/1.1 in that order.
cfg.set_protocols(&[b"h2".to_vec(), b"http/1.1".to_vec()]);
cfg.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
sync::Arc::new(cfg)
};

Expand Down Expand Up @@ -107,7 +107,9 @@ fn load_certs(filename: &str) -> io::Result<Vec<rustls::Certificate>> {
let mut reader = io::BufReader::new(certfile);

// Load and return certificate.
pemfile::certs(&mut reader).map_err(|_| error("failed to load certificate".into()))
let certs = rustls_pemfile::certs(&mut reader)
.map_err(|_| error("failed to load certificate".into()))?;
Ok(certs.into_iter().map(rustls::Certificate).collect())
}

// Load private key from file.
Expand All @@ -118,10 +120,11 @@ fn load_private_key(filename: &str) -> io::Result<rustls::PrivateKey> {
let mut reader = io::BufReader::new(keyfile);

// Load and return a single private key.
let keys = pemfile::rsa_private_keys(&mut reader)
let keys = rustls_pemfile::rsa_private_keys(&mut reader)
.map_err(|_| error("failed to load private key".into()))?;
if keys.len() != 1 {
return Err(error("expected a single private key".into()));
}
Ok(keys[0].clone())

Ok(rustls::PrivateKey(keys[0].clone()))
}
63 changes: 63 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
use rustls::{ClientConfig, ConfigBuilder, WantsVerifier};

/// Methods for configuring roots
///
/// This adds methods (gated by crate features) for easily configuring
/// TLS server roots a rustls ClientConfig will trust.
pub trait ConfigBuilderExt {
/// This configures the platform's trusted certs, as implemented by
/// rustls-native-certs
#[cfg(feature = "rustls-native-certs")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-native-certs")))]
fn with_native_roots(self) -> ClientConfig;

/// This configures the webpki roots, which are Mozilla's set of
/// trusted roots as packaged by webpki-roots.
#[cfg(feature = "webpki-roots")]
#[cfg_attr(docsrs, doc(cfg(feature = "webpki-roots")))]
fn with_webpki_roots(self) -> ClientConfig;
}

impl ConfigBuilderExt for ConfigBuilder<ClientConfig, WantsVerifier> {
#[cfg(feature = "rustls-native-certs")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-native-certs")))]
fn with_native_roots(self) -> ClientConfig {
let mut roots = rustls::RootCertStore::empty();
let mut valid_count = 0;
let mut invalid_count = 0;

for cert in rustls_native_certs::load_native_certs().expect("could not load platform certs")
{
let cert = rustls::Certificate(cert.0);
match roots.add(&cert) {
Ok(_) => valid_count += 1,
Err(err) => {
log::trace!("invalid cert der {:?}", cert.0);
log::debug!("certificate parsing failed: {:?}", err);
invalid_count += 1
}
}
}
log::debug!(
"with_native_roots processed {} valid and {} invalid certs",
valid_count, invalid_count
);
assert!(!roots.is_empty(), "no CA certificates found");

self.with_root_certificates(roots).with_no_client_auth()
g2p marked this conversation as resolved.
Show resolved Hide resolved
}

#[cfg(feature = "webpki-roots")]
#[cfg_attr(docsrs, doc(cfg(feature = "webpki-roots")))]
fn with_webpki_roots(self) -> ClientConfig {
let mut roots = rustls::RootCertStore::empty();
roots.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0.iter().map(|ta| {
rustls::OwnedTrustAnchor::from_subject_spki_name_constraints(
ta.subject,
ta.spki,
ta.name_constraints,
)
}));
self.with_root_certificates(roots).with_no_client_auth()
g2p marked this conversation as resolved.
Show resolved Hide resolved
}
}
62 changes: 7 additions & 55 deletions src/connector.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
#[cfg(feature = "tokio-runtime")]
use hyper::client::connect::HttpConnector;
use hyper::{client::connect::Connection, service::Service, Uri};
use rustls::ClientConfig;
use std::convert::TryFrom;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use std::{fmt, io};

#[cfg(feature = "tokio-runtime")]
use hyper::client::connect::HttpConnector;
use hyper::{client::connect::Connection, service::Service, Uri};
use rustls::ClientConfig;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio_rustls::TlsConnector;
use webpki::DNSNameRef;

use crate::stream::MaybeHttpsStream;

Expand All @@ -28,61 +29,12 @@ pub struct HttpsConnector<T> {
feature = "tokio-runtime"
))]
impl HttpsConnector<HttpConnector> {
/// Construct a new `HttpsConnector` using the OS root store
#[cfg(feature = "rustls-native-certs")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-native-certs")))]
pub fn with_native_roots() -> Self {
let mut config = ClientConfig::new();
config.root_store = match rustls_native_certs::load_native_certs() {
Ok(store) => store,
Err((Some(store), err)) => {
log::warn!("Could not load all certificates: {:?}", err);
store
}
Err((None, err)) => Err(err).expect("cannot access native cert store"),
};
if config.root_store.is_empty() {
panic!("no CA certificates found");
}
Self::build(config)
}

/// Construct a new `HttpsConnector` using the `webpki_roots`
#[cfg(feature = "webpki-roots")]
#[cfg_attr(docsrs, doc(cfg(feature = "webpki-roots")))]
pub fn with_webpki_roots() -> Self {
let mut config = ClientConfig::new();
config
.root_store
.add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS);
Self::build(config)
}

/// Force the use of HTTPS when connecting.
///
/// If a URL is not `https` when connecting, an error is returned. Disabled by default.
pub fn https_only(&mut self, enable: bool) {
self.force_https = enable;
}

fn build(mut config: ClientConfig) -> Self {
let mut http = HttpConnector::new();
http.enforce_http(false);

config.alpn_protocols.clear();
#[cfg(feature = "http2")]
{
config.alpn_protocols.push(b"h2".to_vec());
}

#[cfg(feature = "http1")]
{
config.alpn_protocols.push(b"http/1.1".to_vec());
}

config.ct_logs = Some(&ct_logs::LOGS);
(http, config).into()
}
}

impl<T> fmt::Debug for HttpsConnector<T> {
Expand Down Expand Up @@ -152,7 +104,7 @@ where
let f = async move {
let tcp = connecting_future.await.map_err(Into::into)?;
let connector = TlsConnector::from(cfg);
let dnsname = DNSNameRef::try_from_ascii_str(&hostname)
let dnsname = rustls::ServerName::try_from(hostname.as_str())
.map_err(|_| io::Error::new(io::ErrorKind::Other, "invalid dnsname"))?;
let tls = connector
.connect(dnsname, tcp)
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@

#![cfg_attr(docsrs, feature(doc_cfg))]

mod config;
mod connector;
mod stream;

pub use crate::config::ConfigBuilderExt;
pub use crate::connector::HttpsConnector;
pub use crate::stream::MaybeHttpsStream;
3 changes: 1 addition & 2 deletions src/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ use std::task::{Context, Poll};

use hyper::client::connect::{Connected, Connection};

use rustls::Session;
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tokio_rustls::client::TlsStream;

Expand All @@ -24,7 +23,7 @@ impl<T: AsyncRead + AsyncWrite + Connection + Unpin> Connection for MaybeHttpsSt
MaybeHttpsStream::Http(s) => s.connected(),
MaybeHttpsStream::Https(s) => {
let (tcp, tls) = s.get_ref();
if tls.get_alpn_protocol() == Some(b"h2") {
if tls.alpn_protocol() == Some(b"h2") {
tcp.connected().negotiated_h2()
} else {
tcp.connected()
Expand Down