Skip to content

Commit 5ad7850

Browse files
authored
Merge pull request sfackler#360 from sfackler/channel-binding
Support SCRAM channel binding for Postgres 11
2 parents 9762eb6 + 11ffcac commit 5ad7850

File tree

13 files changed

+188
-45
lines changed

13 files changed

+188
-45
lines changed

.circleci/config.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
- image: rust:1.23.0
2727
environment:
2828
RUSTFLAGS: -D warnings
29-
- image: sfackler/rust-postgres-test:3
29+
- image: sfackler/rust-postgres-test:4
3030
steps:
3131
- checkout
3232
- *RESTORE_REGISTRY

docker-compose.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
version: '2'
22
services:
33
postgres:
4-
image: "sfackler/rust-postgres-test:3"
4+
image: "sfackler/rust-postgres-test:4"
55
ports:
66
- 5433:5433

docker/Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
FROM postgres:10.0
1+
FROM postgres:11-beta1
22

33
COPY sql_setup.sh /docker-entrypoint-initdb.d/

postgres-openssl/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ version = "0.1.0"
44
authors = ["Steven Fackler <[email protected]>"]
55

66
[dependencies]
7-
openssl = "0.10"
7+
openssl = "0.10.9"
88

99
postgres = { version = "0.15", path = "../postgres" }

postgres-openssl/src/lib.rs

+17
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ pub extern crate openssl;
22
extern crate postgres;
33

44
use openssl::error::ErrorStack;
5+
use openssl::hash::MessageDigest;
6+
use openssl::nid::Nid;
57
use openssl::ssl::{ConnectConfiguration, SslConnector, SslMethod, SslStream};
68
use postgres::tls::{Stream, TlsHandshake, TlsStream};
79
use std::error::Error;
@@ -84,4 +86,19 @@ impl TlsStream for OpenSslStream {
8486
fn get_mut(&mut self) -> &mut Stream {
8587
self.0.get_mut()
8688
}
89+
90+
fn tls_server_end_point(&self) -> Option<Vec<u8>> {
91+
let cert = self.0.ssl().peer_certificate()?;
92+
let algo_nid = cert.signature_algorithm().object().nid();
93+
let signature_algorithms = algo_nid.signature_algorithms()?;
94+
95+
let md = match signature_algorithms.digest {
96+
Nid::MD5 | Nid::SHA1 => MessageDigest::sha256(),
97+
nid => MessageDigest::from_nid(nid)?,
98+
};
99+
100+
let digest = cert.digest(md).ok()?;
101+
102+
Some(digest.to_vec())
103+
}
87104
}

postgres-openssl/src/test.rs

+14-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use postgres::{Connection, TlsMode};
44
use OpenSsl;
55

66
#[test]
7-
fn test_require_ssl_conn() {
7+
fn require() {
88
let mut builder = SslConnector::builder(SslMethod::tls()).unwrap();
99
builder.set_ca_file("../test/server.crt").unwrap();
1010
let negotiator = OpenSsl::with_connector(builder.build());
@@ -16,7 +16,7 @@ fn test_require_ssl_conn() {
1616
}
1717

1818
#[test]
19-
fn test_prefer_ssl_conn() {
19+
fn prefer() {
2020
let mut builder = SslConnector::builder(SslMethod::tls()).unwrap();
2121
builder.set_ca_file("../test/server.crt").unwrap();
2222
let negotiator = OpenSsl::with_connector(builder.build());
@@ -26,3 +26,15 @@ fn test_prefer_ssl_conn() {
2626
).unwrap();
2727
conn.execute("SELECT 1::VARCHAR", &[]).unwrap();
2828
}
29+
30+
#[test]
31+
fn scram_user() {
32+
let mut builder = SslConnector::builder(SslMethod::tls()).unwrap();
33+
builder.set_ca_file("../test/server.crt").unwrap();
34+
let negotiator = OpenSsl::with_connector(builder.build());
35+
let conn = Connection::connect(
36+
"postgres://scram_user:password@localhost:5433/postgres",
37+
TlsMode::Require(&negotiator),
38+
).unwrap();
39+
conn.execute("SELECT 1::VARCHAR", &[]).unwrap();
40+
}

postgres-protocol/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,6 @@ generic-array = "0.11"
1616
hmac = "0.6"
1717
md5 = "0.3"
1818
memchr = "2.0"
19-
rand = "0.4"
19+
rand = "0.5"
2020
sha2 = "0.7"
2121
stringprep = "0.1"

postgres-protocol/src/authentication/sasl.rs

+89-22
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use base64;
44
use generic_array::typenum::U32;
55
use generic_array::GenericArray;
66
use hmac::{Hmac, Mac};
7-
use rand::{OsRng, Rng};
7+
use rand::{self, Rng};
88
use sha2::{Digest, Sha256};
99
use std::fmt::Write;
1010
use std::io;
@@ -17,6 +17,8 @@ const NONCE_LENGTH: usize = 24;
1717

1818
/// The identifier of the SCRAM-SHA-256 SASL authentication mechanism.
1919
pub const SCRAM_SHA_256: &'static str = "SCRAM-SHA-256";
20+
/// The identifier of the SCRAM-SHA-256-PLUS SASL authentication mechanism.
21+
pub const SCRAM_SHA_256_PLUS: &'static str = "SCRAM-SHA-256-PLUS";
2022

2123
// since postgres passwords are not required to exclude saslprep-prohibited
2224
// 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<u8, U32> {
5456
hi
5557
}
5658

59+
enum ChannelBindingInner {
60+
Unrequested,
61+
Unsupported,
62+
TlsUnique(Vec<u8>),
63+
TlsServerEndPoint(Vec<u8>),
64+
}
65+
66+
/// The channel binding configuration for a SCRAM authentication exchange.
67+
pub struct ChannelBinding(ChannelBindingInner);
68+
69+
impl ChannelBinding {
70+
/// The server did not request channel binding.
71+
pub fn unrequested() -> ChannelBinding {
72+
ChannelBinding(ChannelBindingInner::Unrequested)
73+
}
74+
75+
/// The server requested channel binding but the client is unable to provide it.
76+
pub fn unsupported() -> ChannelBinding {
77+
ChannelBinding(ChannelBindingInner::Unsupported)
78+
}
79+
80+
/// The server requested channel binding and the client will use the `tls-unique` method.
81+
pub fn tls_unique(finished: Vec<u8>) -> ChannelBinding {
82+
ChannelBinding(ChannelBindingInner::TlsUnique(finished))
83+
}
84+
85+
/// The server requested channel binding and the client will use the `tls-server-end-point`
86+
/// method.
87+
pub fn tls_server_end_point(signature: Vec<u8>) -> ChannelBinding {
88+
ChannelBinding(ChannelBindingInner::TlsServerEndPoint(signature))
89+
}
90+
91+
fn gs2_header(&self) -> &'static str {
92+
match self.0 {
93+
ChannelBindingInner::Unrequested => "y,,",
94+
ChannelBindingInner::Unsupported => "n,,",
95+
ChannelBindingInner::TlsUnique(_) => "p=tls-unique,,",
96+
ChannelBindingInner::TlsServerEndPoint(_) => "p=tls-server-end-point,,",
97+
}
98+
}
99+
100+
fn cbind_data(&self) -> &[u8] {
101+
match self.0 {
102+
ChannelBindingInner::Unrequested | ChannelBindingInner::Unsupported => &[],
103+
ChannelBindingInner::TlsUnique(ref buf)
104+
| ChannelBindingInner::TlsServerEndPoint(ref buf) => buf,
105+
}
106+
}
107+
}
108+
57109
enum State {
58110
Update {
59111
nonce: String,
60112
password: Vec<u8>,
113+
channel_binding: ChannelBinding,
61114
},
62115
Finish {
63116
salted_password: GenericArray<u8, U32>,
@@ -66,7 +119,8 @@ enum State {
66119
Done,
67120
}
68121

69-
/// A type which handles the client side of the SCRAM-SHA-256 authentication process.
122+
/// A type which handles the client side of the SCRAM-SHA-256/SCRAM-SHA-256-PLUS authentication
123+
/// process.
70124
///
71125
/// During the authentication process, if the backend sends an `AuthenticationSASL` message which
72126
/// includes `SCRAM-SHA-256` as an authentication mechanism, this type can be used.
@@ -85,11 +139,11 @@ pub struct ScramSha256 {
85139
state: State,
86140
}
87141

88-
#[allow(missing_docs)]
89142
impl ScramSha256 {
90143
/// Constructs a new instance which will use the provided password for authentication.
91-
pub fn new(password: &[u8]) -> io::Result<ScramSha256> {
92-
let mut rng = OsRng::new()?;
144+
pub fn new(password: &[u8], channel_binding: ChannelBinding) -> io::Result<ScramSha256> {
145+
// rand 0.5's ThreadRng is cryptographically secure
146+
let mut rng = rand::thread_rng();
93147
let nonce = (0..NONCE_LENGTH)
94148
.map(|_| {
95149
let mut v = rng.gen_range(0x21u8, 0x7e);
@@ -100,21 +154,20 @@ impl ScramSha256 {
100154
})
101155
.collect::<String>();
102156

103-
ScramSha256::new_inner(password, nonce)
157+
ScramSha256::new_inner(password, channel_binding, nonce)
104158
}
105159

106-
fn new_inner(password: &[u8], nonce: String) -> io::Result<ScramSha256> {
107-
// the docs say to use pg_same_as_startup_message as the username, but
108-
// psql uses an empty string, so we'll go with that.
109-
let message = format!("n,,n=,r={}", nonce);
110-
111-
let password = normalize(password);
112-
160+
fn new_inner(
161+
password: &[u8],
162+
channel_binding: ChannelBinding,
163+
nonce: String,
164+
) -> io::Result<ScramSha256> {
113165
Ok(ScramSha256 {
114-
message: message,
166+
message: format!("{}n=,r={}", channel_binding.gs2_header(), nonce),
115167
state: State::Update {
116-
nonce: nonce,
117-
password: password,
168+
nonce,
169+
password: normalize(password),
170+
channel_binding: channel_binding,
118171
},
119172
})
120173
}
@@ -131,10 +184,15 @@ impl ScramSha256 {
131184
///
132185
/// This should be called when an `AuthenticationSASLContinue` message is received.
133186
pub fn update(&mut self, message: &[u8]) -> io::Result<()> {
134-
let (client_nonce, password) = match mem::replace(&mut self.state, State::Done) {
135-
State::Update { nonce, password } => (nonce, password),
136-
_ => return Err(io::Error::new(io::ErrorKind::Other, "invalid SCRAM state")),
137-
};
187+
let (client_nonce, password, channel_binding) =
188+
match mem::replace(&mut self.state, State::Done) {
189+
State::Update {
190+
nonce,
191+
password,
192+
channel_binding,
193+
} => (nonce, password, channel_binding),
194+
_ => return Err(io::Error::new(io::ErrorKind::Other, "invalid SCRAM state")),
195+
};
138196

139197
let message =
140198
str::from_utf8(message).map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
@@ -161,8 +219,13 @@ impl ScramSha256 {
161219
hash.input(client_key.as_slice());
162220
let stored_key = hash.result();
163221

222+
let mut cbind_input = vec![];
223+
cbind_input.extend(channel_binding.gs2_header().as_bytes());
224+
cbind_input.extend(channel_binding.cbind_data());
225+
let cbind_input = base64::encode(&cbind_input);
226+
164227
self.message.clear();
165-
write!(&mut self.message, "c=biws,r={}", parsed.nonce).unwrap();
228+
write!(&mut self.message, "c={},r={}", cbind_input, parsed.nonce).unwrap();
166229

167230
let auth_message = format!("n=,r={},{},{}", client_nonce, message, self.message);
168231

@@ -420,7 +483,11 @@ mod test {
420483
1NTlQYNs5BTeQjdHdk7lOflDo5re2an8=";
421484
let server_final = "v=U+ppxD5XUKtradnv8e2MkeupiA8FU87Sg8CXzXHDAzw=";
422485

423-
let mut scram = ScramSha256::new_inner(password.as_bytes(), nonce.to_string()).unwrap();
486+
let mut scram = ScramSha256::new_inner(
487+
password.as_bytes(),
488+
ChannelBinding::unsupported(),
489+
nonce.to_string(),
490+
).unwrap();
424491
assert_eq!(str::from_utf8(scram.message()).unwrap(), client_first);
425492

426493
scram.update(server_first.as_bytes()).unwrap();

postgres/src/lib.rs

+37-10
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ extern crate socket2;
8181

8282
use fallible_iterator::FallibleIterator;
8383
use postgres_protocol::authentication;
84-
use postgres_protocol::authentication::sasl::{self, ScramSha256};
84+
use postgres_protocol::authentication::sasl::{self, ChannelBinding, ScramSha256};
8585
use postgres_protocol::message::backend::{self, ErrorFields};
8686
use postgres_protocol::message::frontend;
8787
use postgres_shared::rows::RowData;
@@ -422,25 +422,52 @@ impl InnerConnection {
422422
self.stream.flush()?;
423423
}
424424
backend::Message::AuthenticationSasl(body) => {
425-
// count to validate the entire message body.
426-
if body
427-
.mechanisms()
428-
.filter(|m| *m == sasl::SCRAM_SHA_256)
429-
.count()? == 0
430-
{
425+
let mut has_scram = false;
426+
let mut has_scram_plus = false;
427+
let mut mechanisms = body.mechanisms();
428+
while let Some(mechanism) = mechanisms.next()? {
429+
match mechanism {
430+
sasl::SCRAM_SHA_256 => has_scram = true,
431+
sasl::SCRAM_SHA_256_PLUS => has_scram_plus = true,
432+
_ => {}
433+
}
434+
}
435+
let channel_binding = self
436+
.stream
437+
.get_ref()
438+
.tls_unique()
439+
.map(ChannelBinding::tls_unique)
440+
.or_else(|| {
441+
self.stream
442+
.get_ref()
443+
.tls_server_end_point()
444+
.map(ChannelBinding::tls_server_end_point)
445+
});
446+
447+
let (channel_binding, mechanism) = if has_scram_plus {
448+
match channel_binding {
449+
Some(channel_binding) => (channel_binding, sasl::SCRAM_SHA_256_PLUS),
450+
None => (ChannelBinding::unsupported(), sasl::SCRAM_SHA_256),
451+
}
452+
} else if has_scram {
453+
match channel_binding {
454+
Some(_) => (ChannelBinding::unrequested(), sasl::SCRAM_SHA_256),
455+
None => (ChannelBinding::unsupported(), sasl::SCRAM_SHA_256),
456+
}
457+
} else {
431458
return Err(
432459
io::Error::new(io::ErrorKind::Other, "unsupported authentication").into(),
433460
);
434-
}
461+
};
435462

436463
let pass = user.password().ok_or_else(|| {
437464
error::connect("a password was requested but not provided".into())
438465
})?;
439466

440-
let mut scram = ScramSha256::new(pass.as_bytes())?;
467+
let mut scram = ScramSha256::new(pass.as_bytes(), channel_binding)?;
441468

442469
self.stream.write_message(|buf| {
443-
frontend::sasl_initial_response(sasl::SCRAM_SHA_256, scram.message(), buf)
470+
frontend::sasl_initial_response(mechanism, scram.message(), buf)
444471
})?;
445472
self.stream.flush()?;
446473

postgres/src/tls.rs

+20
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,26 @@ pub trait TlsStream: fmt::Debug + Read + Write + Send {
1212

1313
/// Returns a mutable reference to the underlying `Stream`.
1414
fn get_mut(&mut self) -> &mut Stream;
15+
16+
/// Returns the data associated with the `tls-unique` channel binding type as described in
17+
/// [RFC 5929], if supported.
18+
///
19+
/// An implementation only needs to support one of this or `tls_server_end_point`.
20+
///
21+
/// [RFC 5929]: https://tools.ietf.org/html/rfc5929
22+
fn tls_unique(&self) -> Option<Vec<u8>> {
23+
None
24+
}
25+
26+
/// Returns the data associated with the `tls-server-end-point` channel binding type as
27+
/// described in [RFC 5929], if supported.
28+
///
29+
/// An implementation only needs to support one of this or `tls_unique`.
30+
///
31+
/// [RFC 5929]: https://tools.ietf.org/html/rfc5929
32+
fn tls_server_end_point(&self) -> Option<Vec<u8>> {
33+
None
34+
}
1535
}
1636

1737
/// A trait implemented by types that can initiate a TLS session over a Postgres

tokio-postgres/Cargo.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ tokio-core = "0.1.8"
4444
tokio-dns-unofficial = "0.1"
4545
tokio-io = "0.1"
4646

47-
tokio-openssl = { version = "0.1", optional = true }
48-
openssl = { version = "0.9.23", optional = true }
47+
tokio-openssl = { version = "0.2", optional = true }
48+
openssl = { version = "0.10", optional = true }
4949

5050
[target.'cfg(unix)'.dependencies]
5151
tokio-uds = "0.1"

0 commit comments

Comments
 (0)