diff --git a/.circleci/config.yml b/.circleci/config.yml index eb7a8ea3..7152e1be 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -26,7 +26,7 @@ jobs: - image: rust:1.23.0 environment: RUSTFLAGS: -D warnings - - image: sfackler/rust-postgres-test:3 + - image: sfackler/rust-postgres-test:4 steps: - checkout - *RESTORE_REGISTRY diff --git a/docker-compose.yml b/docker-compose.yml index f61e91cf..c149834e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ version: '2' services: postgres: - image: "sfackler/rust-postgres-test:3" + image: "sfackler/rust-postgres-test:4" ports: - 5433:5433 diff --git a/docker/Dockerfile b/docker/Dockerfile index f73c3de7..9e2642ba 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,3 +1,3 @@ -FROM postgres:10.0 +FROM postgres:11-beta1 COPY sql_setup.sh /docker-entrypoint-initdb.d/ diff --git a/postgres-openssl/Cargo.toml b/postgres-openssl/Cargo.toml index 3cf12cd8..53321fd5 100644 --- a/postgres-openssl/Cargo.toml +++ b/postgres-openssl/Cargo.toml @@ -4,6 +4,6 @@ version = "0.1.0" authors = ["Steven Fackler "] [dependencies] -openssl = "0.10" +openssl = "0.10.9" postgres = { version = "0.15", path = "../postgres" } diff --git a/postgres-openssl/src/lib.rs b/postgres-openssl/src/lib.rs index 8b01e83a..450eb637 100644 --- a/postgres-openssl/src/lib.rs +++ b/postgres-openssl/src/lib.rs @@ -2,6 +2,8 @@ pub extern crate openssl; extern crate postgres; use openssl::error::ErrorStack; +use openssl::hash::MessageDigest; +use openssl::nid::Nid; use openssl::ssl::{ConnectConfiguration, SslConnector, SslMethod, SslStream}; use postgres::tls::{Stream, TlsHandshake, TlsStream}; use std::error::Error; @@ -84,4 +86,19 @@ impl TlsStream for OpenSslStream { fn get_mut(&mut self) -> &mut Stream { self.0.get_mut() } + + fn tls_server_end_point(&self) -> Option> { + let cert = self.0.ssl().peer_certificate()?; + let algo_nid = cert.signature_algorithm().object().nid(); + let signature_algorithms = algo_nid.signature_algorithms()?; + + let md = match signature_algorithms.digest { + Nid::MD5 | Nid::SHA1 => MessageDigest::sha256(), + nid => MessageDigest::from_nid(nid)?, + }; + + let digest = cert.digest(md).ok()?; + + Some(digest.to_vec()) + } } diff --git a/postgres-openssl/src/test.rs b/postgres-openssl/src/test.rs index 5c8e849b..8314e179 100644 --- a/postgres-openssl/src/test.rs +++ b/postgres-openssl/src/test.rs @@ -4,7 +4,7 @@ use postgres::{Connection, TlsMode}; use OpenSsl; #[test] -fn test_require_ssl_conn() { +fn require() { let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); builder.set_ca_file("../test/server.crt").unwrap(); let negotiator = OpenSsl::with_connector(builder.build()); @@ -16,7 +16,7 @@ fn test_require_ssl_conn() { } #[test] -fn test_prefer_ssl_conn() { +fn prefer() { let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); builder.set_ca_file("../test/server.crt").unwrap(); let negotiator = OpenSsl::with_connector(builder.build()); @@ -26,3 +26,15 @@ fn test_prefer_ssl_conn() { ).unwrap(); conn.execute("SELECT 1::VARCHAR", &[]).unwrap(); } + +#[test] +fn scram_user() { + let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); + builder.set_ca_file("../test/server.crt").unwrap(); + let negotiator = OpenSsl::with_connector(builder.build()); + let conn = Connection::connect( + "postgres://scram_user:password@localhost:5433/postgres", + TlsMode::Require(&negotiator), + ).unwrap(); + conn.execute("SELECT 1::VARCHAR", &[]).unwrap(); +} diff --git a/postgres-protocol/Cargo.toml b/postgres-protocol/Cargo.toml index 7865bc76..6ab4c007 100644 --- a/postgres-protocol/Cargo.toml +++ b/postgres-protocol/Cargo.toml @@ -16,6 +16,6 @@ generic-array = "0.11" hmac = "0.6" md5 = "0.3" memchr = "2.0" -rand = "0.4" +rand = "0.5" sha2 = "0.7" stringprep = "0.1" diff --git a/postgres-protocol/src/authentication/sasl.rs b/postgres-protocol/src/authentication/sasl.rs index 32f6be84..617a288f 100644 --- a/postgres-protocol/src/authentication/sasl.rs +++ b/postgres-protocol/src/authentication/sasl.rs @@ -4,7 +4,7 @@ use base64; use generic_array::typenum::U32; use generic_array::GenericArray; use hmac::{Hmac, Mac}; -use rand::{OsRng, Rng}; +use rand::{self, Rng}; use sha2::{Digest, Sha256}; use std::fmt::Write; use std::io; @@ -17,6 +17,8 @@ const NONCE_LENGTH: usize = 24; /// The identifier of the SCRAM-SHA-256 SASL authentication mechanism. pub const SCRAM_SHA_256: &'static str = "SCRAM-SHA-256"; +/// The identifier of the SCRAM-SHA-256-PLUS SASL authentication mechanism. +pub const SCRAM_SHA_256_PLUS: &'static str = "SCRAM-SHA-256-PLUS"; // since postgres passwords are not required to exclude saslprep-prohibited // characters or even be valid UTF8, we run saslprep if possible and otherwise @@ -54,10 +56,61 @@ fn hi(str: &[u8], salt: &[u8], i: u32) -> GenericArray { hi } +enum ChannelBindingInner { + Unrequested, + Unsupported, + TlsUnique(Vec), + TlsServerEndPoint(Vec), +} + +/// The channel binding configuration for a SCRAM authentication exchange. +pub struct ChannelBinding(ChannelBindingInner); + +impl ChannelBinding { + /// The server did not request channel binding. + pub fn unrequested() -> ChannelBinding { + ChannelBinding(ChannelBindingInner::Unrequested) + } + + /// The server requested channel binding but the client is unable to provide it. + pub fn unsupported() -> ChannelBinding { + ChannelBinding(ChannelBindingInner::Unsupported) + } + + /// The server requested channel binding and the client will use the `tls-unique` method. + pub fn tls_unique(finished: Vec) -> ChannelBinding { + ChannelBinding(ChannelBindingInner::TlsUnique(finished)) + } + + /// The server requested channel binding and the client will use the `tls-server-end-point` + /// method. + pub fn tls_server_end_point(signature: Vec) -> ChannelBinding { + ChannelBinding(ChannelBindingInner::TlsServerEndPoint(signature)) + } + + fn gs2_header(&self) -> &'static str { + match self.0 { + ChannelBindingInner::Unrequested => "y,,", + ChannelBindingInner::Unsupported => "n,,", + ChannelBindingInner::TlsUnique(_) => "p=tls-unique,,", + ChannelBindingInner::TlsServerEndPoint(_) => "p=tls-server-end-point,,", + } + } + + fn cbind_data(&self) -> &[u8] { + match self.0 { + ChannelBindingInner::Unrequested | ChannelBindingInner::Unsupported => &[], + ChannelBindingInner::TlsUnique(ref buf) + | ChannelBindingInner::TlsServerEndPoint(ref buf) => buf, + } + } +} + enum State { Update { nonce: String, password: Vec, + channel_binding: ChannelBinding, }, Finish { salted_password: GenericArray, @@ -66,7 +119,8 @@ enum State { Done, } -/// A type which handles the client side of the SCRAM-SHA-256 authentication process. +/// A type which handles the client side of the SCRAM-SHA-256/SCRAM-SHA-256-PLUS authentication +/// process. /// /// During the authentication process, if the backend sends an `AuthenticationSASL` message which /// includes `SCRAM-SHA-256` as an authentication mechanism, this type can be used. @@ -85,11 +139,11 @@ pub struct ScramSha256 { state: State, } -#[allow(missing_docs)] impl ScramSha256 { /// Constructs a new instance which will use the provided password for authentication. - pub fn new(password: &[u8]) -> io::Result { - let mut rng = OsRng::new()?; + pub fn new(password: &[u8], channel_binding: ChannelBinding) -> io::Result { + // rand 0.5's ThreadRng is cryptographically secure + let mut rng = rand::thread_rng(); let nonce = (0..NONCE_LENGTH) .map(|_| { let mut v = rng.gen_range(0x21u8, 0x7e); @@ -100,21 +154,20 @@ impl ScramSha256 { }) .collect::(); - ScramSha256::new_inner(password, nonce) + ScramSha256::new_inner(password, channel_binding, nonce) } - fn new_inner(password: &[u8], nonce: String) -> io::Result { - // the docs say to use pg_same_as_startup_message as the username, but - // psql uses an empty string, so we'll go with that. - let message = format!("n,,n=,r={}", nonce); - - let password = normalize(password); - + fn new_inner( + password: &[u8], + channel_binding: ChannelBinding, + nonce: String, + ) -> io::Result { Ok(ScramSha256 { - message: message, + message: format!("{}n=,r={}", channel_binding.gs2_header(), nonce), state: State::Update { - nonce: nonce, - password: password, + nonce, + password: normalize(password), + channel_binding: channel_binding, }, }) } @@ -131,10 +184,15 @@ impl ScramSha256 { /// /// This should be called when an `AuthenticationSASLContinue` message is received. pub fn update(&mut self, message: &[u8]) -> io::Result<()> { - let (client_nonce, password) = match mem::replace(&mut self.state, State::Done) { - State::Update { nonce, password } => (nonce, password), - _ => return Err(io::Error::new(io::ErrorKind::Other, "invalid SCRAM state")), - }; + let (client_nonce, password, channel_binding) = + match mem::replace(&mut self.state, State::Done) { + State::Update { + nonce, + password, + channel_binding, + } => (nonce, password, channel_binding), + _ => return Err(io::Error::new(io::ErrorKind::Other, "invalid SCRAM state")), + }; let message = str::from_utf8(message).map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?; @@ -161,8 +219,13 @@ impl ScramSha256 { hash.input(client_key.as_slice()); let stored_key = hash.result(); + let mut cbind_input = vec![]; + cbind_input.extend(channel_binding.gs2_header().as_bytes()); + cbind_input.extend(channel_binding.cbind_data()); + let cbind_input = base64::encode(&cbind_input); + self.message.clear(); - write!(&mut self.message, "c=biws,r={}", parsed.nonce).unwrap(); + write!(&mut self.message, "c={},r={}", cbind_input, parsed.nonce).unwrap(); let auth_message = format!("n=,r={},{},{}", client_nonce, message, self.message); @@ -420,7 +483,11 @@ mod test { 1NTlQYNs5BTeQjdHdk7lOflDo5re2an8="; let server_final = "v=U+ppxD5XUKtradnv8e2MkeupiA8FU87Sg8CXzXHDAzw="; - let mut scram = ScramSha256::new_inner(password.as_bytes(), nonce.to_string()).unwrap(); + let mut scram = ScramSha256::new_inner( + password.as_bytes(), + ChannelBinding::unsupported(), + nonce.to_string(), + ).unwrap(); assert_eq!(str::from_utf8(scram.message()).unwrap(), client_first); scram.update(server_first.as_bytes()).unwrap(); diff --git a/postgres/src/lib.rs b/postgres/src/lib.rs index 04fa7663..eec482bf 100644 --- a/postgres/src/lib.rs +++ b/postgres/src/lib.rs @@ -81,7 +81,7 @@ extern crate socket2; use fallible_iterator::FallibleIterator; use postgres_protocol::authentication; -use postgres_protocol::authentication::sasl::{self, ScramSha256}; +use postgres_protocol::authentication::sasl::{self, ChannelBinding, ScramSha256}; use postgres_protocol::message::backend::{self, ErrorFields}; use postgres_protocol::message::frontend; use postgres_shared::rows::RowData; @@ -422,25 +422,52 @@ impl InnerConnection { self.stream.flush()?; } backend::Message::AuthenticationSasl(body) => { - // count to validate the entire message body. - if body - .mechanisms() - .filter(|m| *m == sasl::SCRAM_SHA_256) - .count()? == 0 - { + let mut has_scram = false; + let mut has_scram_plus = false; + let mut mechanisms = body.mechanisms(); + while let Some(mechanism) = mechanisms.next()? { + match mechanism { + sasl::SCRAM_SHA_256 => has_scram = true, + sasl::SCRAM_SHA_256_PLUS => has_scram_plus = true, + _ => {} + } + } + let channel_binding = self + .stream + .get_ref() + .tls_unique() + .map(ChannelBinding::tls_unique) + .or_else(|| { + self.stream + .get_ref() + .tls_server_end_point() + .map(ChannelBinding::tls_server_end_point) + }); + + let (channel_binding, mechanism) = if has_scram_plus { + match channel_binding { + Some(channel_binding) => (channel_binding, sasl::SCRAM_SHA_256_PLUS), + None => (ChannelBinding::unsupported(), sasl::SCRAM_SHA_256), + } + } else if has_scram { + match channel_binding { + Some(_) => (ChannelBinding::unrequested(), sasl::SCRAM_SHA_256), + None => (ChannelBinding::unsupported(), sasl::SCRAM_SHA_256), + } + } else { return Err( io::Error::new(io::ErrorKind::Other, "unsupported authentication").into(), ); - } + }; let pass = user.password().ok_or_else(|| { error::connect("a password was requested but not provided".into()) })?; - let mut scram = ScramSha256::new(pass.as_bytes())?; + let mut scram = ScramSha256::new(pass.as_bytes(), channel_binding)?; self.stream.write_message(|buf| { - frontend::sasl_initial_response(sasl::SCRAM_SHA_256, scram.message(), buf) + frontend::sasl_initial_response(mechanism, scram.message(), buf) })?; self.stream.flush()?; diff --git a/postgres/src/tls.rs b/postgres/src/tls.rs index beb639b4..2c051a61 100644 --- a/postgres/src/tls.rs +++ b/postgres/src/tls.rs @@ -12,6 +12,26 @@ pub trait TlsStream: fmt::Debug + Read + Write + Send { /// Returns a mutable reference to the underlying `Stream`. fn get_mut(&mut self) -> &mut Stream; + + /// Returns the data associated with the `tls-unique` channel binding type as described in + /// [RFC 5929], if supported. + /// + /// An implementation only needs to support one of this or `tls_server_end_point`. + /// + /// [RFC 5929]: https://tools.ietf.org/html/rfc5929 + fn tls_unique(&self) -> Option> { + None + } + + /// Returns the data associated with the `tls-server-end-point` channel binding type as + /// described in [RFC 5929], if supported. + /// + /// An implementation only needs to support one of this or `tls_unique`. + /// + /// [RFC 5929]: https://tools.ietf.org/html/rfc5929 + fn tls_server_end_point(&self) -> Option> { + None + } } /// A trait implemented by types that can initiate a TLS session over a Postgres diff --git a/tokio-postgres/Cargo.toml b/tokio-postgres/Cargo.toml index 432c048e..fd3e6864 100644 --- a/tokio-postgres/Cargo.toml +++ b/tokio-postgres/Cargo.toml @@ -44,8 +44,8 @@ tokio-core = "0.1.8" tokio-dns-unofficial = "0.1" tokio-io = "0.1" -tokio-openssl = { version = "0.1", optional = true } -openssl = { version = "0.9.23", optional = true } +tokio-openssl = { version = "0.2", optional = true } +openssl = { version = "0.10", optional = true } [target.'cfg(unix)'.dependencies] tokio-uds = "0.1" diff --git a/tokio-postgres/src/test.rs b/tokio-postgres/src/test.rs index 055bc46a..588bffd3 100644 --- a/tokio-postgres/src/test.rs +++ b/tokio-postgres/src/test.rs @@ -270,10 +270,10 @@ fn ssl_user_ssl_required() { #[cfg(feature = "with-openssl")] #[test] fn openssl_required() { - use tls::openssl::openssl::ssl::{SslConnectorBuilder, SslMethod}; + use tls::openssl::openssl::ssl::{SslConnector, SslMethod}; use tls::openssl::OpenSsl; - let mut builder = SslConnectorBuilder::new(SslMethod::tls()).unwrap(); + let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); builder.set_ca_file("../test/server.crt").unwrap(); let negotiator = OpenSsl::from(builder.build()); diff --git a/tokio-postgres/src/tls/openssl.rs b/tokio-postgres/src/tls/openssl.rs index 4b351bac..f62418f5 100644 --- a/tokio-postgres/src/tls/openssl.rs +++ b/tokio-postgres/src/tls/openssl.rs @@ -3,7 +3,7 @@ extern crate tokio_openssl; pub extern crate openssl; use futures::Future; -use self::openssl::ssl::{SslMethod, SslConnector, SslConnectorBuilder}; +use self::openssl::ssl::{SslMethod, SslConnector}; use self::openssl::error::ErrorStack; use std::error::Error; use self::tokio_openssl::{SslConnectorExt, SslStream}; @@ -27,7 +27,7 @@ pub struct OpenSsl(SslConnector); impl OpenSsl { /// Creates a new `OpenSsl` with default settings. pub fn new() -> Result { - let connector = SslConnectorBuilder::new(SslMethod::tls())?.build(); + let connector = SslConnector::builder(SslMethod::tls())?.build(); Ok(OpenSsl(connector)) } }