Skip to content

Commit

Permalink
Refactor SSL support into two modules
Browse files Browse the repository at this point in the history
Pull as much of the implementation specific code for OpenSSL and Rustls
into their own modules, then re-export either of them as
`SslContextImpl` and `SslStream`, where there is an implicit trait
contract between the two implementations.
  • Loading branch information
bradfier committed Feb 25, 2022
1 parent 3e2ca09 commit a9823f2
Show file tree
Hide file tree
Showing 8 changed files with 352 additions and 189 deletions.
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,7 @@ zeroize = { version = "1.5.2", optional = true }
rustc-serialize = "0.3"
sha1 = "0.6.0"
fdlimit = "0.1"

[package.metadata.docs.rs]
# Enable just one SSL implementation
features = ["ssl-openssl"]
2 changes: 1 addition & 1 deletion examples/ssl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@ fn main() {
.respond(response)
.unwrap_or(println!("Failed to respond to request"));
}
}
}
121 changes: 22 additions & 99 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,9 @@
#![forbid(unsafe_code)]
#![deny(rust_2018_idioms)]
#![allow(clippy::match_like_matches_macro)]
#[cfg(feature = "ssl-openssl")]
extern crate openssl;

#[cfg(feature = "ssl-rustls")]
extern crate rustls;
#[cfg(feature = "ssl-rustls")]
extern crate rustls_pemfile;
#[cfg(any(feature = "ssl-openssl", feature = "ssl-rustls"))]
use zeroize::Zeroizing;

use std::error::Error;
use std::io::Error as IoError;
Expand All @@ -123,6 +119,7 @@ mod client;
mod common;
mod request;
mod response;
mod ssl;
mod test;
mod util;

Expand Down Expand Up @@ -246,90 +243,28 @@ impl Server {
};

// building the SSL capabilities
#[cfg(feature = "ssl-openssl")]
type SslContext = openssl::ssl::SslContext;
#[cfg(feature = "ssl-rustls")]
type SslContext = Arc<rustls::ServerConfig>;
#[cfg(all(feature = "ssl-openssl", feature = "ssl-rustls"))]
compile_error!(
"Features 'ssl-openssl' and 'ssl-rustls' must not be enabled at the same time"
);
#[cfg(not(any(feature = "ssl-openssl", feature = "ssl-rustls")))]
type SslContext = ();
let ssl: Option<SslContext> = match ssl_config {
#[cfg(feature = "ssl-openssl")]
Some(mut config) => {
use openssl::pkey::PKey;
use openssl::ssl;
use openssl::ssl::SslVerifyMode;
use openssl::x509::X509;
use zeroize::Zeroize;

let mut ctxt = SslContext::builder(ssl::SslMethod::tls())?;
ctxt.set_cipher_list("DEFAULT")?;
let certificate = X509::from_pem(&config.certificate[..])?;
ctxt.set_certificate(&certificate)?;
let private_key = PKey::private_key_from_pem(&config.private_key[..])?;
ctxt.set_private_key(&private_key)?;
ctxt.set_verify(SslVerifyMode::NONE);
ctxt.check_private_key()?;

// Remove the private key from memory after handing it to OpenSSL
config.private_key.zeroize();

Some(ctxt.build())
}
#[cfg(feature = "ssl-rustls")]
Some(config) => {
use zeroize::Zeroizing;

let certificate_chain: Vec<rustls::Certificate> =
rustls_pemfile::certs(&mut config.certificate.as_slice())?
.into_iter()
.map(|bytes| rustls::Certificate(bytes))
.collect();

if certificate_chain.is_empty() {
return Err("Couldn't extract certificate chain from config.".into());
}

// Extract the private key into a type that is reliably zero-ed on Drop, since we
// need to clone it a bunch of times. We have to trust that the compiler will
// perform a move here, it would be better for config.private_key to itself be a
// `Zeroizing` type, but that's a breaking API change for not much benefit.
let zeroizing_private_key = Zeroizing::new(config.private_key);
// Both `rustls_pemfile` functions consume the input slice so the clones sprinkled
// throughout are necessary.
let private_key = rustls::PrivateKey({
let pkcs8_keys = rustls_pemfile::pkcs8_private_keys(
&mut zeroizing_private_key.clone().as_slice(),
)
.expect(
"file contains invalid pkcs8 private key (encrypted keys are not supported)",
);
// prefer to load pkcs8 keys
if let Some(pkcs8_key) = pkcs8_keys.first() {
pkcs8_key.clone()
} else {
let rsa_keys = rustls_pemfile::rsa_private_keys(
&mut zeroizing_private_key.clone().as_slice(),
)
.expect("file contains invalid rsa private key");
rsa_keys[0].clone()
}
});

let tls_conf = rustls::ServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth()
.with_single_cert(certificate_chain, private_key)?;

Some(Arc::new(tls_conf))
}
#[cfg(not(any(feature = "ssl-openssl", feature = "ssl-rustls")))]
Some(_) => {
return Err(
#[cfg(any(feature = "ssl-openssl", feature = "ssl-rustls"))]
type SslContext = crate::ssl::SslContextImpl;
let ssl: Option<SslContext> = {
match ssl_config {
#[cfg(any(feature = "ssl-openssl", feature = "ssl-rustls"))]
Some(config) => Some(SslContext::from_pem(
config.certificate,
Zeroizing::new(config.private_key),
)?),
#[cfg(not(any(feature = "ssl-openssl", feature = "ssl-rustls")))]
Some(_) => return Err(
"Building a server with SSL requires enabling the `ssl` feature in tiny-http"
.into(),
)
),
None => None,
}
None => None,
};

// creating a task where server.accept() is continuously called
Expand All @@ -349,9 +284,8 @@ impl Server {
use util::RefinedTcpStream;
let (read_closable, write_closable) = match ssl {
None => RefinedTcpStream::new(sock),
#[cfg(feature = "ssl-openssl")]
#[cfg(any(feature = "ssl-openssl", feature = "ssl-rustls"))]
Some(ref ssl) => {
let ssl = openssl::ssl::Ssl::new(ssl).expect("Couldn't create ssl");
// trying to apply SSL over the connection
// if an error occurs, we just close the socket and resume listening
let sock = match ssl.accept(sock) {
Expand All @@ -361,19 +295,8 @@ impl Server {

RefinedTcpStream::new(sock)
}
#[cfg(feature = "ssl-rustls")]
Some(ref tls_conf) => {
let tls_session =
match rustls::ServerConnection::new(tls_conf.clone()) {
Ok(s) => s,
Err(_) => continue,
};
let stream = rustls::StreamOwned::new(tls_session, sock);

RefinedTcpStream::new(stream)
}
#[cfg(not(any(feature = "ssl-openssl", feature = "ssl-rustls")))]
Some(_) => unreachable!(),
Some(ref _ssl) => unreachable!(),
};

Ok(ClientConnection::new(write_closable, read_closable))
Expand Down
20 changes: 20 additions & 0 deletions src/ssl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//! Modules providing SSL/TLS implementations. For backwards compatibility, OpenSSL is the default
//! implementation, but Rustls is highly recommended as a pure Rust alternative.
//!
//! In order to simplify the swappable implementations these SSL/TLS modules adhere to an implicit
//! trait contract and specific implementations are re-exported as [`SslContextImpl`] and [`SslStream`].
//! The concrete type of these aliases will depend on which module you enable in `Cargo.toml`.

#[cfg(feature = "ssl-openssl")]
pub(crate) mod openssl;
#[cfg(feature = "ssl-openssl")]
pub(crate) use self::openssl::OpenSslContext as SslContextImpl;
#[cfg(feature = "ssl-openssl")]
pub(crate) use self::openssl::SplitOpenSslStream as SslStream;

#[cfg(feature = "ssl-rustls")]
pub(crate) mod rustls;
#[cfg(feature = "ssl-rustls")]
pub(crate) use self::rustls::RustlsContext as SslContextImpl;
#[cfg(feature = "ssl-rustls")]
pub(crate) use self::rustls::RustlsStream as SslStream;
102 changes: 102 additions & 0 deletions src/ssl/openssl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
use crate::util::refined_tcp_stream::Stream as RefinedStream;
use std::error::Error;
use std::io::{Read, Write};
use std::net::{Shutdown, SocketAddr, TcpStream};
use std::sync::{Arc, Mutex};
use zeroize::Zeroizing;

pub(crate) struct OpenSslStream {
inner: openssl::ssl::SslStream<TcpStream>,
}

/// An OpenSSL stream which has been split into two mutually exclusive streams (e.g. for read / write)
pub(crate) struct SplitOpenSslStream(Arc<Mutex<OpenSslStream>>);

// These struct methods form the implict contract for swappable TLS implementations
impl SplitOpenSslStream {
pub(crate) fn peer_addr(&mut self) -> std::io::Result<SocketAddr> {
self.0.lock().unwrap().inner.get_mut().peer_addr()
}

pub(crate) fn shutdown(&mut self, how: Shutdown) -> std::io::Result<()> {
self.0.lock().unwrap().inner.get_mut().shutdown(how)
}
}

impl Clone for SplitOpenSslStream {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}

impl Read for SplitOpenSslStream {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().read(buf)
}
}

impl Write for SplitOpenSslStream {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().write(buf)
}

fn flush(&mut self) -> std::io::Result<()> {
self.0.lock().unwrap().flush()
}
}

impl Read for OpenSslStream {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.inner.read(buf)
}
}

impl Write for OpenSslStream {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.inner.write(buf)
}

fn flush(&mut self) -> std::io::Result<()> {
self.inner.flush()
}
}

pub(crate) struct OpenSslContext(openssl::ssl::SslContext);

impl OpenSslContext {
pub fn from_pem(
certificates: Vec<u8>,
private_key: Zeroizing<Vec<u8>>,
) -> Result<Self, Box<dyn Error + Send + Sync>> {
use openssl::pkey::PKey;
use openssl::ssl::{self, SslVerifyMode};
use openssl::x509::X509;

let mut ctx = openssl::ssl::SslContext::builder(ssl::SslMethod::tls())?;
ctx.set_cipher_list("DEFAULT")?;
let cert = X509::from_pem(&certificates)?;
ctx.set_certificate(&cert)?;
let key = PKey::private_key_from_pem(&private_key)?;
ctx.set_private_key(&key)?;
ctx.set_verify(SslVerifyMode::NONE);
ctx.check_private_key()?;

Ok(Self(ctx.build()))
}

pub fn accept(
&self,
stream: TcpStream,
) -> Result<OpenSslStream, Box<dyn Error + Send + Sync + 'static>> {
use openssl::ssl::Ssl;
let session = Ssl::new(&self.0).expect("Failed to create new OpenSSL session");
let stream = session.accept(stream)?;
Ok(OpenSslStream { inner: stream })
}
}

impl From<OpenSslStream> for RefinedStream {
fn from(stream: OpenSslStream) -> Self {
RefinedStream::Https(SplitOpenSslStream(Arc::new(Mutex::new(stream))))
}
}

0 comments on commit a9823f2

Please sign in to comment.