Move tokio-tls into workspace (#529)

This commit is contained in:
Sean McArthur 2018-08-08 08:36:17 -07:00 committed by Carl Lerche
parent c89b0b4c8c
commit afcfefd7e3
11 changed files with 1270 additions and 2 deletions

View File

@ -64,8 +64,8 @@ script:
if [[ "$TARGET" ]]
then
rustup target add $TARGET
cargo check --all --target $TARGET
cargo check --tests --all --target $TARGET
cargo check --all --exclude tokio-tls --target $TARGET
cargo check --tests --all --exclude tokio-tls --target $TARGET
else
cargo test --all
# Disable these tests for now as they are buggy

View File

@ -32,6 +32,7 @@ members = [
"tokio-threadpool",
"tokio-timer",
"tokio-tcp",
"tokio-tls",
"tokio-udp",
"tokio-uds",
]

50
tokio-tls/Cargo.toml Normal file
View File

@ -0,0 +1,50 @@
[package]
name = "tokio-tls"
version = "0.2.0"
authors = ["Carl Lerche <me@carllerche.com>",
"Alex Crichton <alex@alexcrichton.com>"]
license = "MIT"
repository = "https://github.com/tokio-rs/tokio"
homepage = "https://tokio.rs"
documentation = "https://docs.rs/tokio-tls"
description = """
An implementation of TLS/SSL streams for Tokio giving an implementation of TLS
for nonblocking I/O streams.
"""
categories = ["asynchronous", "network-programming"]
[badges]
travis-ci = { repository = "tokio-rs/tokio-tls" }
[dependencies]
futures = "0.1.23"
native-tls = "0.2"
tokio-io = { version = "0.1", path = "../tokio-io" }
[dev-dependencies]
tokio = { version = "0.1", path = "../" }
cfg-if = "0.1"
env_logger = { version = "0.4", default-features = false }
[target.'cfg(all(not(target_os = "macos"), not(windows), not(target_os = "ios")))'.dev-dependencies]
openssl = "0.10"
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dev-dependencies]
security-framework = "0.2"
[target.'cfg(windows)'.dev-dependencies]
schannel = "0.1"
[target.'cfg(windows)'.dev-dependencies.winapi]
version = "0.3"
features = [
"lmcons",
"basetsd",
"minwinbase",
"minwindef",
"ntdef",
"sysinfoapi",
"timezoneapi",
"wincrypt",
"winerror",
]

25
tokio-tls/LICENSE Normal file
View File

@ -0,0 +1,25 @@
Copyright (c) 2018 Tokio Contributors
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

54
tokio-tls/README.md Normal file
View File

@ -0,0 +1,54 @@
# tokio-tls
An implementation of TLS/SSL streams for Tokio built on top of the [`native-tls`
crate]
[Documentation](https://docs.rs/tokio-tls)
[`native-tls` crate]: https://github.com/sfackler/rust-native-tls
## Usage
First, add this to your `Cargo.toml`:
```toml
[dependencies]
native-tls = "0.2"
tokio-tls = "0.2"
```
Next, add this to your crate:
```rust
extern crate native_tls;
extern crate tokio_tls;
use tokio_tls::{TlsConnector, TlsAcceptor};
```
You can find few examples how to use this crate in examples directory (using TLS in
hyper server or client).
By default the `native-tls` crate currently uses the "platform appropriate"
backend for a TLS implementation. This means:
* On Windows, [SChannel] is used
* On OSX, [SecureTransport] is used
* Everywhere else, [OpenSSL] is used
[SChannel]: https://msdn.microsoft.com/en-us/library/windows/desktop/aa380123%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396
[SecureTransport]: https://developer.apple.com/reference/security/1654508-secure_transport
[OpenSSL]: https://www.openssl.org/
Typically these selections mean that you don't have to worry about a portability
when using TLS, these libraries are all normally installed by default.
## License
This project is licensed under the [MIT license](./LICENSE).
### Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in Tokio by you, shall be licensed as MIT, without any additional
terms or conditions.

View File

@ -0,0 +1,41 @@
extern crate futures;
extern crate native_tls;
extern crate tokio;
extern crate tokio_io;
extern crate tokio_tls;
use std::io;
use std::net::ToSocketAddrs;
use futures::Future;
use native_tls::TlsConnector;
use tokio::net::TcpStream;
use tokio::runtime::Runtime;
fn main() {
let mut runtime = Runtime::new().unwrap();
let addr = "www.rust-lang.org:443".to_socket_addrs().unwrap().next().unwrap();
let socket = TcpStream::connect(&addr);
let cx = TlsConnector::builder().build().unwrap();
let cx = tokio_tls::TlsConnector::from(cx);
let tls_handshake = socket.and_then(move |socket| {
cx.connect("www.rust-lang.org", socket).map_err(|e| {
io::Error::new(io::ErrorKind::Other, e)
})
});
let request = tls_handshake.and_then(|socket| {
tokio_io::io::write_all(socket, "\
GET / HTTP/1.0\r\n\
Host: www.rust-lang.org\r\n\
\r\n\
".as_bytes())
});
let response = request.and_then(|(socket, _)| {
tokio_io::io::read_to_end(socket, Vec::new())
});
let (_, data) = runtime.block_on(response).unwrap();
println!("{}", String::from_utf8_lossy(&data));
}

Binary file not shown.

213
tokio-tls/src/lib.rs Normal file
View File

@ -0,0 +1,213 @@
//! Async TLS streams
//!
//! This library is an implementation of TLS streams using the most appropriate
//! system library by default for negotiating the connection. That is, on
//! Windows this library uses SChannel, on OSX it uses SecureTransport, and on
//! other platforms it uses OpenSSL.
//!
//! Each TLS stream implements the `Read` and `Write` traits to interact and
//! interoperate with the rest of the futures I/O ecosystem. Client connections
//! initiated from this crate verify hostnames automatically and by default.
//!
//! This crate primarily exports this ability through two newtypes,
//! `TlsConnector` and `TlsAcceptor`. These newtypes augment the
//! functionality provided by the `native-tls` crate, on which this crate is
//! built. Configuration of TLS parameters is still primarily done through the
//! `native-tls` crate.
#![deny(missing_docs)]
#![doc(html_root_url = "https://docs.rs/tokio-tls/0.1")]
extern crate futures;
extern crate native_tls;
#[macro_use]
extern crate tokio_io;
use std::io::{self, Read, Write};
use futures::{Poll, Future, Async};
use native_tls::{HandshakeError, Error};
use tokio_io::{AsyncRead, AsyncWrite};
/// A wrapper around an underlying raw stream which implements the TLS or SSL
/// protocol.
///
/// A `TlsStream<S>` represents a handshake that has been completed successfully
/// and both the server and the client are ready for receiving and sending
/// data. Bytes read from a `TlsStream` are decrypted from `S` and bytes written
/// to a `TlsStream` are encrypted when passing through to `S`.
#[derive(Debug)]
pub struct TlsStream<S> {
inner: native_tls::TlsStream<S>,
}
/// A wrapper around a `native_tls::TlsConnector`, providing an async `connect`
/// method.
pub struct TlsConnector {
inner: native_tls::TlsConnector,
}
/// A wrapper around a `native_tls::TlsAcceptor`, providing an async `accept`
/// method.
pub struct TlsAcceptor {
inner: native_tls::TlsAcceptor,
}
/// Future returned from `TlsConnector::connect` which will resolve
/// once the connection handshake has finished.
pub struct Connect<S> {
inner: MidHandshake<S>,
}
/// Future returned from `TlsAcceptor::accept` which will resolve
/// once the accept handshake has finished.
pub struct Accept<S> {
inner: MidHandshake<S>,
}
struct MidHandshake<S> {
inner: Option<Result<native_tls::TlsStream<S>, HandshakeError<S>>>,
}
impl<S> TlsStream<S> {
/// Get access to the internal `native_tls::TlsStream` stream which also
/// transitively allows access to `S`.
pub fn get_ref(&self) -> &native_tls::TlsStream<S> {
&self.inner
}
/// Get mutable access to the internal `native_tls::TlsStream` stream which
/// also transitively allows mutable access to `S`.
pub fn get_mut(&mut self) -> &mut native_tls::TlsStream<S> {
&mut self.inner
}
}
impl<S: Read + Write> Read for TlsStream<S> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.inner.read(buf)
}
}
impl<S: Read + Write> Write for TlsStream<S> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.inner.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.inner.flush()
}
}
impl<S: AsyncRead + AsyncWrite> AsyncRead for TlsStream<S> {
}
impl<S: AsyncRead + AsyncWrite> AsyncWrite for TlsStream<S> {
fn shutdown(&mut self) -> Poll<(), io::Error> {
try_nb!(self.inner.shutdown());
self.inner.get_mut().shutdown()
}
}
impl TlsConnector {
/// Connects the provided stream with this connector, assuming the provided
/// domain.
///
/// This function will internally call `TlsConnector::connect` to connect
/// the stream and returns a future representing the resolution of the
/// connection operation. The returned future will resolve to either
/// `TlsStream<S>` or `Error` depending if it's successful or not.
///
/// This is typically used for clients who have already established, for
/// example, a TCP connection to a remote server. That stream is then
/// provided here to perform the client half of a connection to a
/// TLS-powered server.
pub fn connect<S>(&self, domain: &str, stream: S) -> Connect<S>
where S: AsyncRead + AsyncWrite,
{
Connect {
inner: MidHandshake {
inner: Some(self.inner.connect(domain, stream)),
},
}
}
}
impl From<native_tls::TlsConnector> for TlsConnector {
fn from(inner: native_tls::TlsConnector) -> TlsConnector {
TlsConnector {
inner,
}
}
}
impl TlsAcceptor {
/// Accepts a new client connection with the provided stream.
///
/// This function will internally call `TlsAcceptor::accept` to connect
/// the stream and returns a future representing the resolution of the
/// connection operation. The returned future will resolve to either
/// `TlsStream<S>` or `Error` depending if it's successful or not.
///
/// This is typically used after a new socket has been accepted from a
/// `TcpListener`. That socket is then passed to this function to perform
/// the server half of accepting a client connection.
pub fn accept<S>(&self, stream: S) -> Accept<S>
where S: AsyncRead + AsyncWrite,
{
Accept {
inner: MidHandshake {
inner: Some(self.inner.accept(stream)),
},
}
}
}
impl From<native_tls::TlsAcceptor> for TlsAcceptor {
fn from(inner: native_tls::TlsAcceptor) -> TlsAcceptor {
TlsAcceptor {
inner,
}
}
}
impl<S: AsyncRead + AsyncWrite> Future for Connect<S> {
type Item = TlsStream<S>;
type Error = Error;
fn poll(&mut self) -> Poll<TlsStream<S>, Error> {
self.inner.poll()
}
}
impl<S: AsyncRead + AsyncWrite> Future for Accept<S> {
type Item = TlsStream<S>;
type Error = Error;
fn poll(&mut self) -> Poll<TlsStream<S>, Error> {
self.inner.poll()
}
}
impl<S: AsyncRead + AsyncWrite> Future for MidHandshake<S> {
type Item = TlsStream<S>;
type Error = Error;
fn poll(&mut self) -> Poll<TlsStream<S>, Error> {
match self.inner.take().expect("cannot poll MidHandshake twice") {
Ok(stream) => Ok(TlsStream { inner: stream }.into()),
Err(HandshakeError::Failure(e)) => Err(e),
Err(HandshakeError::WouldBlock(s)) => {
match s.handshake() {
Ok(stream) => Ok(TlsStream { inner: stream }.into()),
Err(HandshakeError::Failure(e)) => Err(e),
Err(HandshakeError::WouldBlock(s)) => {
self.inner = Some(Err(HandshakeError::WouldBlock(s)));
Ok(Async::NotReady)
}
}
}
}
}
}

134
tokio-tls/tests/bad.rs Normal file
View File

@ -0,0 +1,134 @@
extern crate env_logger;
extern crate futures;
extern crate native_tls;
extern crate tokio;
extern crate tokio_tls;
#[macro_use]
extern crate cfg_if;
use std::io::{self, Error};
use std::net::ToSocketAddrs;
use futures::Future;
use native_tls::TlsConnector;
use tokio::net::TcpStream;
use tokio::runtime::Runtime;
macro_rules! t {
($e:expr) => (match $e {
Ok(e) => e,
Err(e) => panic!("{} failed with {:?}", stringify!($e), e),
})
}
cfg_if! {
if #[cfg(feature = "force-rustls")] {
fn verify_failed(err: &Error, s: &str) {
let err = err.to_string();
assert!(err.contains(s), "bad error: {}", err);
}
fn assert_expired_error(err: &Error) {
verify_failed(err, "CertExpired");
}
fn assert_wrong_host(err: &Error) {
verify_failed(err, "CertNotValidForName");
}
fn assert_self_signed(err: &Error) {
verify_failed(err, "UnknownIssuer");
}
fn assert_untrusted_root(err: &Error) {
verify_failed(err, "UnknownIssuer");
}
} else if #[cfg(any(feature = "force-openssl",
all(not(target_os = "macos"),
not(target_os = "windows"),
not(target_os = "ios"))))] {
extern crate openssl;
fn verify_failed(err: &Error) {
assert!(format!("{}", err).contains("certificate verify failed"))
}
use verify_failed as assert_expired_error;
use verify_failed as assert_wrong_host;
use verify_failed as assert_self_signed;
use verify_failed as assert_untrusted_root;
} else if #[cfg(any(target_os = "macos", target_os = "ios"))] {
fn assert_invalid_cert_chain(err: &Error) {
assert!(format!("{}", err).contains("was not trusted."))
}
use assert_invalid_cert_chain as assert_expired_error;
use assert_invalid_cert_chain as assert_wrong_host;
use assert_invalid_cert_chain as assert_self_signed;
use assert_invalid_cert_chain as assert_untrusted_root;
} else {
fn assert_expired_error(err: &Error) {
let s = err.to_string();
assert!(s.contains("system clock"), "error = {:?}", s);
}
fn assert_wrong_host(err: &Error) {
let s = err.to_string();
assert!(s.contains("CN name"), "error = {:?}", s);
}
fn assert_self_signed(err: &Error) {
let s = err.to_string();
assert!(s.contains("root certificate which is not trusted"), "error = {:?}", s);
}
use assert_self_signed as assert_untrusted_root;
}
}
fn get_host(host: &'static str) -> Error {
drop(env_logger::init());
let addr = format!("{}:443", host);
let addr = t!(addr.to_socket_addrs()).next().unwrap();
let mut l = t!(Runtime::new());
let client = TcpStream::connect(&addr);
let data = client.and_then(move |socket| {
let builder = TlsConnector::builder();
let cx = builder.build().unwrap();
let cx = tokio_tls::TlsConnector::from(cx);
cx.connect(host, socket).map_err(|e| {
Error::new(io::ErrorKind::Other, e)
})
});
let res = l.block_on(data);
assert!(res.is_err());
res.err().unwrap()
}
#[test]
fn expired() {
assert_expired_error(&get_host("expired.badssl.com"))
}
// TODO: the OSX builders on Travis apparently fail this tests spuriously?
// passes locally though? Seems... bad!
#[test]
#[cfg_attr(all(target_os = "macos", feature = "force-openssl"), ignore)]
fn wrong_host() {
assert_wrong_host(&get_host("wrong.host.badssl.com"))
}
#[test]
fn self_signed() {
assert_self_signed(&get_host("self-signed.badssl.com"))
}
#[test]
fn untrusted_root() {
assert_untrusted_root(&get_host("untrusted-root.badssl.com"))
}

118
tokio-tls/tests/google.rs Normal file
View File

@ -0,0 +1,118 @@
extern crate env_logger;
extern crate futures;
extern crate native_tls;
extern crate tokio_io;
extern crate tokio_tls;
extern crate tokio;
#[macro_use]
extern crate cfg_if;
use std::io;
use std::net::ToSocketAddrs;
use futures::Future;
use native_tls::TlsConnector;
use tokio_io::io::{flush, read_to_end, write_all};
use tokio::net::TcpStream;
use tokio::runtime::Runtime;
macro_rules! t {
($e:expr) => (match $e {
Ok(e) => e,
Err(e) => panic!("{} failed with {:?}", stringify!($e), e),
})
}
cfg_if! {
if #[cfg(feature = "force-rustls")] {
fn assert_bad_hostname_error(err: &io::Error) {
let err = err.to_string();
assert!(err.contains("CertNotValidForName"), "bad error: {}", err);
}
} else if #[cfg(any(feature = "force-openssl",
all(not(target_os = "macos"),
not(target_os = "windows"),
not(target_os = "ios"))))] {
extern crate openssl;
fn assert_bad_hostname_error(err: &io::Error) {
let err = err.get_ref().unwrap();
let err = err.downcast_ref::<native_tls::Error>().unwrap();
assert!(format!("{}", err).contains("certificate verify failed"));
}
} else if #[cfg(any(target_os = "macos", target_os = "ios"))] {
fn assert_bad_hostname_error(err: &io::Error) {
let err = err.get_ref().unwrap();
let err = err.downcast_ref::<native_tls::Error>().unwrap();
assert!(format!("{}", err).contains("was not trusted."));
}
} else {
fn assert_bad_hostname_error(err: &io::Error) {
let err = err.get_ref().unwrap();
let err = err.downcast_ref::<native_tls::Error>().unwrap();
assert!(format!("{}", err).contains("CN name"));
}
}
}
fn native2io(e: native_tls::Error) -> io::Error {
io::Error::new(io::ErrorKind::Other, e)
}
#[test]
fn fetch_google() {
drop(env_logger::init());
// First up, resolve google.com
let addr = t!("google.com:443".to_socket_addrs()).next().unwrap();
// Create an event loop and connect a socket to our resolved address.c
let mut l = t!(Runtime::new());
let client = TcpStream::connect(&addr);
// Send off the request by first negotiating an SSL handshake, then writing
// of our request, then flushing, then finally read off the response.
let data = client.and_then(move |socket| {
let builder = TlsConnector::builder();
let connector = t!(builder.build());
let connector = tokio_tls::TlsConnector::from(connector);
connector.connect("google.com", socket).map_err(native2io)
})
.and_then(|socket| write_all(socket, b"GET / HTTP/1.0\r\n\r\n"))
.and_then(|(socket, _)| flush(socket))
.and_then(|socket| read_to_end(socket, Vec::new()));
let (_, data) = t!(l.block_on(data));
// any response code is fine
assert!(data.starts_with(b"HTTP/1.0 "));
let data = String::from_utf8_lossy(&data);
let data = data.trim_right();
assert!(data.ends_with("</html>") || data.ends_with("</HTML>"));
}
// see comment in bad.rs for ignore reason
#[cfg_attr(all(target_os = "macos", feature = "force-openssl"), ignore)]
#[test]
fn wrong_hostname_error() {
drop(env_logger::init());
let addr = t!("google.com:443".to_socket_addrs()).next().unwrap();
let mut l = t!(Runtime::new());
let client = TcpStream::connect(&addr);
let data = client.and_then(move |socket| {
let builder = TlsConnector::builder();
let connector = t!(builder.build());
let connector = tokio_tls::TlsConnector::from(connector);
connector.connect("rust-lang.org", socket)
.map_err(native2io)
});
let res = l.block_on(data);
assert!(res.is_err());
assert_bad_hostname_error(&res.err().unwrap());
}

632
tokio-tls/tests/smoke.rs Normal file
View File

@ -0,0 +1,632 @@
extern crate env_logger;
extern crate futures;
extern crate native_tls;
extern crate tokio;
extern crate tokio_io;
extern crate tokio_tls;
#[macro_use]
extern crate cfg_if;
use std::io::{self, Read, Write};
use std::process::Command;
use futures::{Future, Poll};
use futures::stream::Stream;
use tokio_io::{AsyncRead, AsyncWrite};
use tokio_io::io::{read_to_end, copy, shutdown};
use tokio::runtime::Runtime;
use tokio::net::{TcpListener, TcpStream};
use native_tls::{TlsConnector, TlsAcceptor, Identity};
macro_rules! t {
($e:expr) => (match $e {
Ok(e) => e,
Err(e) => panic!("{} failed with {:?}", stringify!($e), e),
})
}
#[allow(dead_code)]
struct Keys {
cert_der: Vec<u8>,
pkey_der: Vec<u8>,
pkcs12_der: Vec<u8>,
}
#[allow(dead_code)]
fn openssl_keys() -> &'static Keys {
static INIT: Once = ONCE_INIT;
static mut KEYS: *mut Keys = 0 as *mut _;
INIT.call_once(|| {
let path = t!(env::current_exe());
let path = path.parent().unwrap();
let keyfile = path.join("test.key");
let certfile = path.join("test.crt");
let config = path.join("openssl.config");
File::create(&config).unwrap().write_all(b"\
[req]\n\
distinguished_name=dn\n\
[ dn ]\n\
CN=localhost\n\
[ ext ]\n\
basicConstraints=CA:FALSE,pathlen:0\n\
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
").unwrap();
let subj = "/C=US/ST=Denial/L=Sprintfield/O=Dis/CN=localhost";
let output = t!(Command::new("openssl")
.arg("req")
.arg("-nodes")
.arg("-x509")
.arg("-newkey").arg("rsa:2048")
.arg("-config").arg(&config)
.arg("-extensions").arg("ext")
.arg("-subj").arg(subj)
.arg("-keyout").arg(&keyfile)
.arg("-out").arg(&certfile)
.arg("-days").arg("1")
.output());
assert!(output.status.success());
let crtout = t!(Command::new("openssl")
.arg("x509")
.arg("-outform").arg("der")
.arg("-in").arg(&certfile)
.output());
assert!(crtout.status.success());
let keyout = t!(Command::new("openssl")
.arg("rsa")
.arg("-outform").arg("der")
.arg("-in").arg(&keyfile)
.output());
assert!(keyout.status.success());
let pkcs12out = t!(Command::new("openssl")
.arg("pkcs12")
.arg("-export")
.arg("-nodes")
.arg("-inkey").arg(&keyfile)
.arg("-in").arg(&certfile)
.arg("-password").arg("pass:foobar")
.output());
assert!(pkcs12out.status.success());
let keys = Box::new(Keys {
cert_der: crtout.stdout,
pkey_der: keyout.stdout,
pkcs12_der: pkcs12out.stdout,
});
unsafe {
KEYS = Box::into_raw(keys);
}
});
unsafe {
&*KEYS
}
}
cfg_if! {
if #[cfg(feature = "rustls")] {
extern crate webpki;
extern crate untrusted;
use std::env;
use std::fs::File;
use std::process::Command;
use std::sync::{ONCE_INIT, Once};
use untrusted::Input;
use webpki::trust_anchor_util;
fn server_cx() -> io::Result<ServerContext> {
let mut cx = ServerContext::new();
let (cert, key) = keys();
cx.config_mut()
.set_single_cert(vec![cert.to_vec()], key.to_vec());
Ok(cx)
}
fn configure_client(cx: &mut ClientContext) {
let (cert, _key) = keys();
let cert = Input::from(cert);
let anchor = trust_anchor_util::cert_der_as_trust_anchor(cert).unwrap();
cx.config_mut().root_store.add_trust_anchors(&[anchor]);
}
// Like OpenSSL we generate certificates on the fly, but for OSX we
// also have to put them into a specific keychain. We put both the
// certificates and the keychain next to our binary.
//
// Right now I don't know of a way to programmatically create a
// self-signed certificate, so we just fork out to the `openssl` binary.
fn keys() -> (&'static [u8], &'static [u8]) {
static INIT: Once = ONCE_INIT;
static mut KEYS: *mut (Vec<u8>, Vec<u8>) = 0 as *mut _;
INIT.call_once(|| {
let (key, cert) = openssl_keys();
let path = t!(env::current_exe());
let path = path.parent().unwrap();
let keyfile = path.join("test.key");
let certfile = path.join("test.crt");
let config = path.join("openssl.config");
File::create(&config).unwrap().write_all(b"\
[req]\n\
distinguished_name=dn\n\
[ dn ]\n\
CN=localhost\n\
[ ext ]\n\
basicConstraints=CA:FALSE,pathlen:0\n\
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
").unwrap();
let subj = "/C=US/ST=Denial/L=Sprintfield/O=Dis/CN=localhost";
let output = t!(Command::new("openssl")
.arg("req")
.arg("-nodes")
.arg("-x509")
.arg("-newkey").arg("rsa:2048")
.arg("-config").arg(&config)
.arg("-extensions").arg("ext")
.arg("-subj").arg(subj)
.arg("-keyout").arg(&keyfile)
.arg("-out").arg(&certfile)
.arg("-days").arg("1")
.output());
assert!(output.status.success());
let crtout = t!(Command::new("openssl")
.arg("x509")
.arg("-outform").arg("der")
.arg("-in").arg(&certfile)
.output());
assert!(crtout.status.success());
let keyout = t!(Command::new("openssl")
.arg("rsa")
.arg("-outform").arg("der")
.arg("-in").arg(&keyfile)
.output());
assert!(keyout.status.success());
let cert = crtout.stdout;
let key = keyout.stdout;
unsafe {
KEYS = Box::into_raw(Box::new((cert, key)));
}
});
unsafe {
(&(*KEYS).0, &(*KEYS).1)
}
}
} else if #[cfg(any(feature = "force-openssl",
all(not(target_os = "macos"),
not(target_os = "windows"),
not(target_os = "ios"))))] {
extern crate openssl;
use std::fs::File;
use std::env;
use std::sync::{Once, ONCE_INIT};
fn contexts() -> (tokio_tls::TlsAcceptor, tokio_tls::TlsConnector) {
let keys = openssl_keys();
let pkcs12 = t!(Identity::from_pkcs12(&keys.pkcs12_der, "foobar"));
let srv = TlsAcceptor::builder(pkcs12);
let cert = t!(native_tls::Certificate::from_der(&keys.cert_der));
let mut client = TlsConnector::builder();
t!(client.add_root_certificate(cert).build());
(t!(srv.build()).into(), t!(client.build()).into())
}
} else if #[cfg(any(target_os = "macos", target_os = "ios"))] {
extern crate security_framework;
use std::env;
use std::fs::File;
use std::sync::{Once, ONCE_INIT};
fn contexts() -> (tokio_tls::TlsAcceptor, tokio_tls::TlsConnector) {
let keys = openssl_keys();
let pkcs12 = t!(Identity::from_pkcs12(&keys.pkcs12_der, "foobar"));
let srv = TlsAcceptor::builder(pkcs12);
let cert = native_tls::Certificate::from_der(&keys.cert_der).unwrap();
let mut client = TlsConnector::builder();
client.add_root_certificate(cert);
(t!(srv.build()).into(), t!(client.build()).into())
}
} else {
extern crate schannel;
extern crate winapi;
use std::env;
use std::fs::File;
use std::io::Error;
use std::mem;
use std::ptr;
use std::sync::{Once, ONCE_INIT};
use schannel::cert_context::CertContext;
use schannel::cert_store::{CertStore, CertAdd, Memory};
use winapi::shared::basetsd::*;
use winapi::shared::lmcons::*;
use winapi::shared::minwindef::*;
use winapi::shared::ntdef::WCHAR;
use winapi::um::minwinbase::*;
use winapi::um::sysinfoapi::*;
use winapi::um::timezoneapi::*;
use winapi::um::wincrypt::*;
const FRIENDLY_NAME: &'static str = "tokio-tls localhost testing cert";
fn contexts() -> (tokio_tls::TlsAcceptor, tokio_tls::TlsConnector) {
let cert = localhost_cert();
let mut store = t!(Memory::new()).into_store();
t!(store.add_cert(&cert, CertAdd::Always));
let pkcs12_der = t!(store.export_pkcs12("foobar"));
let pkcs12 = t!(Identity::from_pkcs12(&pkcs12_der, "foobar"));
let srv = TlsAcceptor::builder(pkcs12);
let client = TlsConnector::builder();
(t!(srv.build()).into(), t!(client.build()).into())
}
// ====================================================================
// Magic!
//
// Lots of magic is happening here to wrangle certificates for running
// these tests on Windows. For more information see the test suite
// in the schannel-rs crate as this is just coyping that.
//
// The general gist of this though is that the only way to add custom
// trusted certificates is to add it to the system store of trust. To
// do that we go through the whole rigamarole here to generate a new
// self-signed certificate and then insert that into the system store.
//
// This generates some dialogs, so we print what we're doing sometimes,
// and otherwise we just manage the ephemeral certificates. Because
// they're in the system store we always ensure that they're only valid
// for a small period of time (e.g. 1 day).
fn localhost_cert() -> CertContext {
static INIT: Once = ONCE_INIT;
INIT.call_once(|| {
for cert in local_root_store().certs() {
let name = match cert.friendly_name() {
Ok(name) => name,
Err(_) => continue,
};
if name != FRIENDLY_NAME {
continue
}
if !cert.is_time_valid().unwrap() {
io::stdout().write_all(br#"
The tokio-tls test suite is about to delete an old copy of one of its
certificates from your root trust store. This certificate was only valid for one
day and it is no longer needed. The host should be "localhost" and the
description should mention "tokio-tls".
"#).unwrap();
cert.delete().unwrap();
} else {
return
}
}
install_certificate().unwrap();
});
for cert in local_root_store().certs() {
let name = match cert.friendly_name() {
Ok(name) => name,
Err(_) => continue,
};
if name == FRIENDLY_NAME {
return cert
}
}
panic!("couldn't find a cert");
}
fn local_root_store() -> CertStore {
if env::var("APPVEYOR").is_ok() {
CertStore::open_local_machine("Root").unwrap()
} else {
CertStore::open_current_user("Root").unwrap()
}
}
fn install_certificate() -> io::Result<CertContext> {
unsafe {
let mut provider = 0;
let mut hkey = 0;
let mut buffer = "tokio-tls test suite".encode_utf16()
.chain(Some(0))
.collect::<Vec<_>>();
let res = CryptAcquireContextW(&mut provider,
buffer.as_ptr(),
ptr::null_mut(),
PROV_RSA_FULL,
CRYPT_MACHINE_KEYSET);
if res != TRUE {
// create a new key container (since it does not exist)
let res = CryptAcquireContextW(&mut provider,
buffer.as_ptr(),
ptr::null_mut(),
PROV_RSA_FULL,
CRYPT_NEWKEYSET | CRYPT_MACHINE_KEYSET);
if res != TRUE {
return Err(Error::last_os_error())
}
}
// create a new keypair (RSA-2048)
let res = CryptGenKey(provider,
AT_SIGNATURE,
0x0800<<16 | CRYPT_EXPORTABLE,
&mut hkey);
if res != TRUE {
return Err(Error::last_os_error());
}
// start creating the certificate
let name = "CN=localhost,O=tokio-tls,OU=tokio-tls,\
G=tokio_tls".encode_utf16()
.chain(Some(0))
.collect::<Vec<_>>();
let mut cname_buffer: [WCHAR; UNLEN as usize + 1] = mem::zeroed();
let mut cname_len = cname_buffer.len() as DWORD;
let res = CertStrToNameW(X509_ASN_ENCODING,
name.as_ptr(),
CERT_X500_NAME_STR,
ptr::null_mut(),
cname_buffer.as_mut_ptr() as *mut u8,
&mut cname_len,
ptr::null_mut());
if res != TRUE {
return Err(Error::last_os_error());
}
let mut subject_issuer = CERT_NAME_BLOB {
cbData: cname_len,
pbData: cname_buffer.as_ptr() as *mut u8,
};
let mut key_provider = CRYPT_KEY_PROV_INFO {
pwszContainerName: buffer.as_mut_ptr(),
pwszProvName: ptr::null_mut(),
dwProvType: PROV_RSA_FULL,
dwFlags: CRYPT_MACHINE_KEYSET,
cProvParam: 0,
rgProvParam: ptr::null_mut(),
dwKeySpec: AT_SIGNATURE,
};
let mut sig_algorithm = CRYPT_ALGORITHM_IDENTIFIER {
pszObjId: szOID_RSA_SHA256RSA.as_ptr() as *mut _,
Parameters: mem::zeroed(),
};
let mut expiration_date: SYSTEMTIME = mem::zeroed();
GetSystemTime(&mut expiration_date);
let mut file_time: FILETIME = mem::zeroed();
let res = SystemTimeToFileTime(&mut expiration_date,
&mut file_time);
if res != TRUE {
return Err(Error::last_os_error());
}
let mut timestamp: u64 = file_time.dwLowDateTime as u64 |
(file_time.dwHighDateTime as u64) << 32;
// one day, timestamp unit is in 100 nanosecond intervals
timestamp += (1E9 as u64) / 100 * (60 * 60 * 24);
file_time.dwLowDateTime = timestamp as u32;
file_time.dwHighDateTime = (timestamp >> 32) as u32;
let res = FileTimeToSystemTime(&file_time,
&mut expiration_date);
if res != TRUE {
return Err(Error::last_os_error());
}
// create a self signed certificate
let cert_context = CertCreateSelfSignCertificate(
0 as ULONG_PTR,
&mut subject_issuer,
0,
&mut key_provider,
&mut sig_algorithm,
ptr::null_mut(),
&mut expiration_date,
ptr::null_mut());
if cert_context.is_null() {
return Err(Error::last_os_error());
}
// TODO: this is.. a terrible hack. Right now `schannel`
// doesn't provide a public method to go from a raw
// cert context pointer to the `CertContext` structure it
// has, so we just fake it here with a transmute. This'll
// probably break at some point, but hopefully by then
// it'll have a method to do this!
struct MyCertContext<T>(T);
impl<T> Drop for MyCertContext<T> {
fn drop(&mut self) {}
}
let cert_context = MyCertContext(cert_context);
let cert_context: CertContext = mem::transmute(cert_context);
try!(cert_context.set_friendly_name(FRIENDLY_NAME));
// install the certificate to the machine's local store
io::stdout().write_all(br#"
The tokio-tls test suite is about to add a certificate to your set of root
and trusted certificates. This certificate should be for the domain "localhost"
with the description related to "tokio-tls". This certificate is only valid
for one day and will be automatically deleted if you re-run the tokio-tls
test suite later.
"#).unwrap();
try!(local_root_store().add_cert(&cert_context,
CertAdd::ReplaceExisting));
Ok(cert_context)
}
}
}
}
fn native2io(e: native_tls::Error) -> io::Error {
io::Error::new(io::ErrorKind::Other, e)
}
const AMT: u64 = 128 * 1024;
#[test]
fn client_to_server() {
drop(env_logger::init());
let mut l = t!(Runtime::new());
// Create a server listening on a port, then figure out what that port is
let srv = t!(TcpListener::bind(&t!("127.0.0.1:0".parse())));
let addr = t!(srv.local_addr());
let (server_cx, client_cx) = contexts();
// Create a future to accept one socket, connect the ssl stream, and then
// read all the data from it.
let socket = srv.incoming().take(1).collect();
let received = socket.map(|mut socket| {
socket.remove(0)
}).and_then(move |socket| {
server_cx.accept(socket).map_err(native2io)
}).and_then(|socket| {
read_to_end(socket, Vec::new())
});
// Create a future to connect to our server, connect the ssl stream, and
// then write a bunch of data to it.
let client = TcpStream::connect(&addr);
let sent = client.and_then(move |socket| {
client_cx.connect("localhost", socket).map_err(native2io)
}).and_then(|socket| {
copy(io::repeat(9).take(AMT), socket)
}).and_then(|(amt, _repeat, socket)| {
shutdown(socket).map(move |_| amt)
});
// Finally, run everything!
let (amt, (_, data)) = t!(l.block_on(sent.join(received)));
assert_eq!(amt, AMT);
assert!(data == vec![9; amt as usize]);
}
#[test]
fn server_to_client() {
drop(env_logger::init());
let mut l = t!(Runtime::new());
// Create a server listening on a port, then figure out what that port is
let srv = t!(TcpListener::bind(&t!("127.0.0.1:0".parse())));
let addr = t!(srv.local_addr());
let (server_cx, client_cx) = contexts();
let socket = srv.incoming().take(1).collect();
let sent = socket.map(|mut socket| {
socket.remove(0)
}).and_then(move |socket| {
server_cx.accept(socket).map_err(native2io)
}).and_then(|socket| {
copy(io::repeat(9).take(AMT), socket)
}).and_then(|(amt, _repeat, socket)| {
shutdown(socket).map(move |_| amt)
});
let client = TcpStream::connect(&addr);
let received = client.and_then(move |socket| {
client_cx.connect("localhost", socket).map_err(native2io)
}).and_then(|socket| {
read_to_end(socket, Vec::new())
});
// Finally, run everything!
let (amt, (_, data)) = t!(l.block_on(sent.join(received)));
assert_eq!(amt, AMT);
assert!(data == vec![9; amt as usize]);
}
struct OneByte<S> {
inner: S,
}
impl<S: Read> Read for OneByte<S> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.inner.read(&mut buf[..1])
}
}
impl<S: Write> Write for OneByte<S> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.inner.write(&buf[..1])
}
fn flush(&mut self) -> io::Result<()> {
self.inner.flush()
}
}
impl<S: AsyncRead> AsyncRead for OneByte<S> {}
impl<S: AsyncWrite> AsyncWrite for OneByte<S> {
fn shutdown(&mut self) -> Poll<(), io::Error> {
self.inner.shutdown()
}
}
#[test]
fn one_byte_at_a_time() {
const AMT: u64 = 1024;
drop(env_logger::init());
let mut l = t!(Runtime::new());
let srv = t!(TcpListener::bind(&t!("127.0.0.1:0".parse())));
let addr = t!(srv.local_addr());
let (server_cx, client_cx) = contexts();
let socket = srv.incoming().take(1).collect();
let sent = socket.map(|mut socket| {
socket.remove(0)
}).and_then(move |socket| {
server_cx.accept(OneByte { inner: socket }).map_err(native2io)
}).and_then(|socket| {
copy(io::repeat(9).take(AMT), socket)
}).and_then(|(amt, _repeat, socket)| {
shutdown(socket).map(move |_| amt)
});
let client = TcpStream::connect(&addr);
let received = client.and_then(move |socket| {
let socket = OneByte { inner: socket };
client_cx.connect("localhost", socket).map_err(native2io)
}).and_then(|socket| {
read_to_end(socket, Vec::new())
});
let (amt, (_, data)) = t!(l.block_on(sent.join(received)));
assert_eq!(amt, AMT);
assert!(data == vec![9; amt as usize]);
}