diff --git a/Cargo.toml b/Cargo.toml index 4752836a7..f56e4e18f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "postgres-native-tls", "postgres-openssl", "postgres-protocol", + "postgres-rustls", "postgres-types", "tokio-postgres", ] diff --git a/postgres-rustls/.gitignore b/postgres-rustls/.gitignore new file mode 100644 index 000000000..693699042 --- /dev/null +++ b/postgres-rustls/.gitignore @@ -0,0 +1,3 @@ +/target +**/*.rs.bk +Cargo.lock diff --git a/postgres-rustls/Cargo.toml b/postgres-rustls/Cargo.toml new file mode 100644 index 000000000..9d619645c --- /dev/null +++ b/postgres-rustls/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "tokio-postgres-rustls" +description = "Rustls integration for tokio-postgres" +version = "0.9.0" +authors = ["Jasper Hugo "] +repository = "https://github.com/jbg/tokio-postgres-rustls" +edition = "2018" +license = "MIT" +readme = "README.md" + +[dependencies] +futures = "0.3" +ring = "0.16" +rustls = "0.20" +tokio = "1" +tokio-postgres = "0.7" +tokio-rustls = "0.23" + +[dev-dependencies] +env_logger = { version = "0.9", default-features = false } +tokio = { version = "1", features = ["macros", "rt"] } +rustls = { version = "0.20", features = ["dangerous_configuration"] } diff --git a/postgres-rustls/LICENSE b/postgres-rustls/LICENSE new file mode 100644 index 000000000..f5b2b76ea --- /dev/null +++ b/postgres-rustls/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Jasper Hugo + +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. diff --git a/postgres-rustls/README.md b/postgres-rustls/README.md new file mode 100644 index 000000000..b9b5cafb0 --- /dev/null +++ b/postgres-rustls/README.md @@ -0,0 +1,22 @@ +# tokio-postgres-rustls +This is an integration between the [rustls TLS stack](https://github.com/ctz/rustls) +and the [tokio-postgres asynchronous PostgreSQL client library](https://github.com/sfackler/rust-postgres). + +[![Crate](https://img.shields.io/crates/v/tokio-postgres-rustls.svg)](https://crates.io/crates/tokio-postgres-rustls) + +[API Documentation](https://docs.rs/tokio-postgres-rustls/) + +# Example + +``` +let config = rustls::ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(rustls::RootCertStore::empty()) + .with_no_client_auth(); +let tls = tokio_postgres_rustls::MakeRustlsConnect::new(config); +let connect_fut = tokio_postgres::connect("sslmode=require host=localhost user=postgres", tls); +// ... +``` + +# License +tokio-postgres-rustls is distributed under the MIT license. diff --git a/postgres-rustls/src/lib.rs b/postgres-rustls/src/lib.rs new file mode 100644 index 000000000..a3a4fc235 --- /dev/null +++ b/postgres-rustls/src/lib.rs @@ -0,0 +1,173 @@ +use std::{ + convert::TryFrom, + future::Future, + io, + pin::Pin, + sync::Arc, + task::{Context, Poll}, +}; + +use futures::future::{FutureExt, TryFutureExt}; +use ring::digest; +use rustls::{ClientConfig, ServerName}; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; +use tokio_postgres::tls::{ChannelBinding, MakeTlsConnect, TlsConnect}; +use tokio_rustls::{client::TlsStream, TlsConnector}; + +#[derive(Clone)] +pub struct MakeRustlsConnect { + config: Arc, +} + +impl MakeRustlsConnect { + pub fn new(config: ClientConfig) -> Self { + Self { + config: Arc::new(config), + } + } +} + +impl MakeTlsConnect for MakeRustlsConnect +where + S: AsyncRead + AsyncWrite + Unpin + Send + 'static, +{ + type Stream = RustlsStream; + type TlsConnect = RustlsConnect; + type Error = io::Error; + + fn make_tls_connect(&mut self, hostname: &str) -> io::Result { + ServerName::try_from(hostname) + .map(|dns_name| { + RustlsConnect(Some(RustlsConnectData { + hostname: dns_name, + connector: Arc::clone(&self.config).into(), + })) + }) + .or(Ok(RustlsConnect(None))) + } +} + +pub struct RustlsConnect(Option); + +struct RustlsConnectData { + hostname: ServerName, + connector: TlsConnector, +} + +impl TlsConnect for RustlsConnect +where + S: AsyncRead + AsyncWrite + Unpin + Send + 'static, +{ + type Stream = RustlsStream; + type Error = io::Error; + type Future = Pin>> + Send>>; + + fn connect(self, stream: S) -> Self::Future { + match self.0 { + None => Box::pin(core::future::ready(Err(io::ErrorKind::InvalidInput.into()))), + Some(c) => c + .connector + .connect(c.hostname, stream) + .map_ok(|s| RustlsStream(Box::pin(s))) + .boxed(), + } + } +} + +pub struct RustlsStream(Pin>>); + +impl tokio_postgres::tls::TlsStream for RustlsStream +where + S: AsyncRead + AsyncWrite + Unpin, +{ + fn channel_binding(&self) -> ChannelBinding { + let (_, session) = self.0.get_ref(); + match session.peer_certificates() { + Some(certs) if !certs.is_empty() => { + let sha256 = digest::digest(&digest::SHA256, certs[0].as_ref()); + ChannelBinding::tls_server_end_point(sha256.as_ref().into()) + } + _ => ChannelBinding::none(), + } + } +} + +impl AsyncRead for RustlsStream +where + S: AsyncRead + AsyncWrite + Unpin, +{ + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + self.0.as_mut().poll_read(cx, buf) + } +} + +impl AsyncWrite for RustlsStream +where + S: AsyncRead + AsyncWrite + Unpin, +{ + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context, + buf: &[u8], + ) -> Poll> { + self.0.as_mut().poll_write(cx, buf) + } + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + self.0.as_mut().poll_flush(cx) + } + + fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + self.0.as_mut().poll_shutdown(cx) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use futures::future::TryFutureExt; + use rustls::{client::ServerCertVerified, client::ServerCertVerifier, Certificate, Error}; + use std::time::SystemTime; + + struct AcceptAllVerifier {} + impl ServerCertVerifier for AcceptAllVerifier { + fn verify_server_cert( + &self, + _end_entity: &Certificate, + _intermediates: &[Certificate], + _server_name: &ServerName, + _scts: &mut dyn Iterator, + _ocsp_response: &[u8], + _now: SystemTime, + ) -> Result { + Ok(ServerCertVerified::assertion()) + } + } + + #[tokio::test] + async fn it_works() { + env_logger::builder().is_test(true).try_init().unwrap(); + + let mut config = rustls::ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(rustls::RootCertStore::empty()) + .with_no_client_auth(); + config + .dangerous() + .set_certificate_verifier(Arc::new(AcceptAllVerifier {})); + let tls = super::MakeRustlsConnect::new(config); + let (client, conn) = tokio_postgres::connect( + "sslmode=require host=localhost port=5432 user=postgres", + tls, + ) + .await + .expect("connect"); + tokio::spawn(conn.map_err(|e| panic!("{:?}", e))); + let stmt = client.prepare("SELECT 1").await.expect("prepare"); + let _ = client.query(&stmt, &[]).await.expect("query"); + } +}