Support SCRAM channel binding for Postgres 11 · sfackler/rust-postgres@11ffcac (original) (raw)
`@@ -4,7 +4,7 @@ use base64;
`
4
4
`use generic_array::typenum::U32;
`
5
5
`use generic_array::GenericArray;
`
6
6
`use hmac::{Hmac, Mac};
`
7
``
`-
use rand::{OsRng, Rng};
`
``
7
`+
use rand::{self, Rng};
`
8
8
`use sha2::{Digest, Sha256};
`
9
9
`use std::fmt::Write;
`
10
10
`use std::io;
`
`@@ -17,6 +17,8 @@ const NONCE_LENGTH: usize = 24;
`
17
17
``
18
18
`/// The identifier of the SCRAM-SHA-256 SASL authentication mechanism.
`
19
19
`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";
`
20
22
``
21
23
`// since postgres passwords are not required to exclude saslprep-prohibited
`
22
24
`// 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> {
`
54
56
` hi
`
55
57
`}
`
56
58
``
``
59
`+
enum ChannelBindingInner {
`
``
60
`+
Unrequested,
`
``
61
`+
Unsupported,
`
``
62
`+
TlsUnique(Vec),
`
``
63
`+
TlsServerEndPoint(Vec),
`
``
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) -> 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) -> 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
+
57
109
`enum State {
`
58
110
`Update {
`
59
111
`nonce: String,
`
60
112
`password: Vec,
`
``
113
`+
channel_binding: ChannelBinding,
`
61
114
`},
`
62
115
`Finish {
`
63
116
`salted_password: GenericArray<u8, U32>,
`
`@@ -66,7 +119,8 @@ enum State {
`
66
119
`Done,
`
67
120
`}
`
68
121
``
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.
`
70
124
`///
`
71
125
`` /// During the authentication process, if the backend sends an AuthenticationSASL
message which
``
72
126
`` /// includes SCRAM-SHA-256
as an authentication mechanism, this type can be used.
``
`@@ -85,11 +139,11 @@ pub struct ScramSha256 {
`
85
139
`state: State,
`
86
140
`}
`
87
141
``
88
``
`-
#[allow(missing_docs)]
`
89
142
`impl ScramSha256 {
`
90
143
`/// Constructs a new instance which will use the provided password for authentication.
`
91
``
`-
pub fn new(password: &[u8]) -> io::Result {
`
92
``
`-
let mut rng = OsRng::new()?;
`
``
144
`+
pub fn new(password: &[u8], channel_binding: ChannelBinding) -> io::Result {
`
``
145
`+
// rand 0.5's ThreadRng is cryptographically secure
`
``
146
`+
let mut rng = rand::thread_rng();
`
93
147
`let nonce = (0..NONCE_LENGTH)
`
94
148
`.map(|_| {
`
95
149
`let mut v = rng.gen_range(0x21u8, 0x7e);
`
`@@ -100,21 +154,20 @@ impl ScramSha256 {
`
100
154
`})
`
101
155
`.collect::();
`
102
156
``
103
``
`-
ScramSha256::new_inner(password, nonce)
`
``
157
`+
ScramSha256::new_inner(password, channel_binding, nonce)
`
104
158
`}
`
105
159
``
106
``
`-
fn new_inner(password: &[u8], nonce: String) -> io::Result {
`
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 {
`
113
165
`Ok(ScramSha256 {
`
114
``
`-
message: message,
`
``
166
`+
message: format!("{}n=,r={}", channel_binding.gs2_header(), nonce),
`
115
167
`state: State::Update {
`
116
``
`-
nonce: nonce,
`
117
``
`-
password: password,
`
``
168
`+
nonce,
`
``
169
`+
password: normalize(password),
`
``
170
`+
channel_binding: channel_binding,
`
118
171
`},
`
119
172
`})
`
120
173
`}
`
`@@ -131,10 +184,15 @@ impl ScramSha256 {
`
131
184
`///
`
132
185
`` /// This should be called when an AuthenticationSASLContinue
message is received.
``
133
186
`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
`+
};
`
138
196
``
139
197
`let message =
`
140
198
` str::from_utf8(message).map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
`
`@@ -161,8 +219,13 @@ impl ScramSha256 {
`
161
219
` hash.input(client_key.as_slice());
`
162
220
`let stored_key = hash.result();
`
163
221
``
``
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
+
164
227
`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();
`
166
229
``
167
230
`let auth_message = format!("n=,r={},{},{}", client_nonce, message, self.message);
`
168
231
``
`@@ -420,7 +483,11 @@ mod test {
`
420
483
` 1NTlQYNs5BTeQjdHdk7lOflDo5re2an8=";
`
421
484
`let server_final = "v=U+ppxD5XUKtradnv8e2MkeupiA8FU87Sg8CXzXHDAzw=";
`
422
485
``
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();
`
424
491
`assert_eq!(str::from_utf8(scram.message()).unwrap(), client_first);
`
425
492
``
426
493
` scram.update(server_first.as_bytes()).unwrap();
`