Skip to main content

pg_proto/
scram.rs

1//! Server-side SCRAM-SHA-256 and SCRAM-SHA-256-PLUS verification.
2
3use std::{io, str};
4
5use base64::{Engine as _, engine::general_purpose::STANDARD};
6use bytes::Bytes;
7use hmac::{Hmac, KeyInit as _, Mac as _};
8use rand::RngExt as _;
9use sha2::{Digest as _, Sha256};
10use subtle::ConstantTimeEq as _;
11
12/// SASL mechanism name for SCRAM without channel binding.
13pub const SCRAM_SHA_256: &[u8] = b"SCRAM-SHA-256";
14/// SASL mechanism name for SCRAM with mandatory channel binding.
15pub const SCRAM_SHA_256_PLUS: &[u8] = b"SCRAM-SHA-256-PLUS";
16/// Default and minimum accepted PBKDF2 iteration count.
17pub const DEFAULT_ITERATIONS: u32 = 4096;
18
19/// Channel-binding evidence available to a server-side SCRAM exchange.
20#[derive(Clone, Debug, Eq, PartialEq)]
21pub enum ServerChannelBinding {
22    /// The connection does not provide channel binding.
23    None,
24    /// RFC 5929 `tls-server-end-point` bytes for the terminated TLS transport.
25    TlsServerEndPoint(Vec<u8>),
26}
27
28/// Reusable server policy holding the credential and channel-binding context.
29pub struct ScramServer {
30    password: Vec<u8>,
31    salt: Vec<u8>,
32    iterations: u32,
33    channel_binding: ServerChannelBinding,
34}
35
36/// One nonce-bound SCRAM exchange awaiting the client-final message.
37pub struct ScramExchange {
38    salted_password: [u8; 32],
39    client_first_bare: String,
40    server_first: String,
41    combined_nonce: String,
42    expected_channel_binding: Vec<u8>,
43}
44
45impl ScramServer {
46    /// Creates a server policy with a random 16-byte salt.
47    #[must_use]
48    pub fn new(password: &[u8], channel_binding: ServerChannelBinding) -> Self {
49        let mut salt = vec![0; 16];
50        rand::rng().fill(&mut salt[..]);
51        Self {
52            password: normalize(password),
53            salt,
54            iterations: DEFAULT_ITERATIONS,
55            channel_binding,
56        }
57    }
58
59    /// Creates a deterministic policy, primarily for persisted verifiers and tests.
60    ///
61    /// # Errors
62    ///
63    /// Rejects an iteration count below RFC 7677's recommended minimum.
64    pub fn with_parameters(
65        password: &[u8],
66        salt: Vec<u8>,
67        iterations: u32,
68        channel_binding: ServerChannelBinding,
69    ) -> io::Result<Self> {
70        if iterations < DEFAULT_ITERATIONS {
71            return Err(invalid("SCRAM iteration count is below 4096"));
72        }
73        Ok(Self {
74            password: normalize(password),
75            salt,
76            iterations,
77            channel_binding,
78        })
79    }
80
81    /// Accepts a SASL initial response and creates the server-first challenge.
82    ///
83    /// # Errors
84    ///
85    /// Rejects unsupported mechanisms, malformed attributes, invalid nonces,
86    /// and channel-binding downgrade attempts.
87    pub fn start(
88        &self,
89        mechanism: &[u8],
90        client_first: &[u8],
91    ) -> io::Result<(ScramExchange, Bytes)> {
92        let client_first = str::from_utf8(client_first)
93            .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?;
94        let (gs2_header, client_first_bare) = split_gs2(client_first)?;
95        let plus = match mechanism {
96            SCRAM_SHA_256 => false,
97            SCRAM_SHA_256_PLUS => true,
98            _ => return Err(invalid("unsupported SCRAM mechanism")),
99        };
100        let expected_channel_binding = self.expected_binding(plus, gs2_header)?;
101        let attributes = attributes(client_first_bare)?;
102        reject_mandatory_extension(&attributes)?;
103        let client_nonce = required(&attributes, b'r')?;
104        validate_nonce(client_nonce)?;
105        let _username = required(&attributes, b'n')?;
106
107        let server_nonce = random_nonce();
108        let combined_nonce = format!("{client_nonce}{server_nonce}");
109        let server_first = format!(
110            "r={combined_nonce},s={},i={}",
111            STANDARD.encode(&self.salt),
112            self.iterations
113        );
114        let exchange = ScramExchange {
115            salted_password: hi(&self.password, &self.salt, self.iterations),
116            client_first_bare: client_first_bare.to_owned(),
117            server_first: server_first.clone(),
118            combined_nonce,
119            expected_channel_binding,
120        };
121        Ok((exchange, Bytes::from(server_first)))
122    }
123
124    fn expected_binding(&self, plus: bool, gs2_header: &str) -> io::Result<Vec<u8>> {
125        match (plus, &self.channel_binding) {
126            (true, ServerChannelBinding::TlsServerEndPoint(binding))
127                if gs2_header == "p=tls-server-end-point,," =>
128            {
129                let mut expected = gs2_header.as_bytes().to_vec();
130                expected.extend_from_slice(binding);
131                Ok(expected)
132            }
133            (true, _) => Err(invalid(
134                "SCRAM-PLUS channel binding is unavailable or invalid",
135            )),
136            (false, ServerChannelBinding::TlsServerEndPoint(_)) if gs2_header == "y,," => {
137                Err(invalid("SCRAM channel-binding downgrade detected"))
138            }
139            (false, _) if matches!(gs2_header, "n,," | "y,,") => Ok(gs2_header.as_bytes().to_vec()),
140            (false, _) => Err(invalid("channel binding used with SCRAM-SHA-256")),
141        }
142    }
143}
144
145impl ScramExchange {
146    /// Verifies the client proof and returns the server-final verifier.
147    ///
148    /// # Errors
149    ///
150    /// Rejects malformed attributes, nonce or channel-binding mismatches, and
151    /// invalid client proofs.
152    pub fn finish(self, client_final: &[u8]) -> io::Result<Bytes> {
153        let client_final = str::from_utf8(client_final)
154            .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?;
155        let proof_marker = client_final
156            .rfind(",p=")
157            .ok_or_else(|| invalid("SCRAM client-final has no proof"))?;
158        let without_proof = &client_final[..proof_marker];
159        let attributes = attributes(client_final)?;
160        reject_mandatory_extension(&attributes)?;
161        if required(&attributes, b'r')? != self.combined_nonce {
162            return Err(invalid("SCRAM nonce mismatch"));
163        }
164        let channel_binding = STANDARD
165            .decode(required(&attributes, b'c')?)
166            .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?;
167        if !bool::from(channel_binding.ct_eq(&self.expected_channel_binding)) {
168            return Err(invalid("SCRAM channel binding mismatch"));
169        }
170        let encoded_proof = required(&attributes, b'p')?;
171        if proof_marker + 3 + encoded_proof.len() != client_final.len() {
172            return Err(invalid("SCRAM proof is not the final attribute"));
173        }
174        let proof = STANDARD
175            .decode(encoded_proof)
176            .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?;
177        if proof.len() != 32 {
178            return Err(invalid("SCRAM proof is not 32 bytes"));
179        }
180
181        let auth_message = format!(
182            "{},{},{}",
183            self.client_first_bare, self.server_first, without_proof
184        );
185        let client_key = hmac(&self.salted_password, b"Client Key");
186        let stored_key = Sha256::digest(client_key);
187        let client_signature = hmac(&stored_key, auth_message.as_bytes());
188        let mut recovered_key = [0; 32];
189        for ((recovered, proof), signature) in
190            recovered_key.iter_mut().zip(proof).zip(client_signature)
191        {
192            *recovered = proof ^ signature;
193        }
194        let recovered_stored_key = Sha256::digest(recovered_key);
195        if !bool::from(recovered_stored_key.ct_eq(&stored_key)) {
196            return Err(invalid("invalid SCRAM client proof"));
197        }
198
199        let server_key = hmac(&self.salted_password, b"Server Key");
200        let server_signature = hmac(&server_key, auth_message.as_bytes());
201        Ok(Bytes::from(format!(
202            "v={}",
203            STANDARD.encode(server_signature)
204        )))
205    }
206}
207
208fn split_gs2(message: &str) -> io::Result<(&str, &str)> {
209    let first = message
210        .find(',')
211        .ok_or_else(|| invalid("malformed SCRAM GS2 header"))?;
212    let second = message[first + 1..]
213        .find(',')
214        .map(|position| first + 1 + position)
215        .ok_or_else(|| invalid("malformed SCRAM GS2 header"))?;
216    Ok((&message[..=second], &message[second + 1..]))
217}
218
219fn attributes(message: &str) -> io::Result<Vec<(u8, &str)>> {
220    let mut output = Vec::new();
221    for attribute in message.split(',') {
222        let bytes = attribute.as_bytes();
223        if bytes.len() < 2 || bytes[1] != b'=' || !bytes[0].is_ascii_alphabetic() {
224            return Err(invalid("malformed SCRAM attribute"));
225        }
226        if output.iter().any(|(name, _)| *name == bytes[0]) {
227            return Err(invalid("duplicate SCRAM attribute"));
228        }
229        output.push((bytes[0], &attribute[2..]));
230    }
231    Ok(output)
232}
233
234fn required<'a>(attributes: &[(u8, &'a str)], name: u8) -> io::Result<&'a str> {
235    attributes
236        .iter()
237        .find_map(|(candidate, value)| (*candidate == name).then_some(*value))
238        .ok_or_else(|| invalid("required SCRAM attribute is missing"))
239}
240
241fn reject_mandatory_extension(attributes: &[(u8, &str)]) -> io::Result<()> {
242    if attributes.iter().any(|(name, _)| *name == b'm') {
243        Err(invalid("unsupported mandatory SCRAM extension"))
244    } else {
245        Ok(())
246    }
247}
248
249fn validate_nonce(nonce: &str) -> io::Result<()> {
250    if !nonce.is_empty()
251        && nonce
252            .bytes()
253            .all(|byte| matches!(byte, 0x21..=0x2b | 0x2d..=0x7e))
254    {
255        Ok(())
256    } else {
257        Err(invalid("invalid SCRAM nonce"))
258    }
259}
260
261fn random_nonce() -> String {
262    let mut rng = rand::rng();
263    (0..24)
264        .map(|_| {
265            let mut byte = rng.random_range(0x21_u8..0x7f);
266            if byte == b',' {
267                byte = b'~';
268            }
269            char::from(byte)
270        })
271        .collect()
272}
273
274fn normalize(password: &[u8]) -> Vec<u8> {
275    let Ok(password) = str::from_utf8(password) else {
276        return password.to_vec();
277    };
278    stringprep::saslprep(password).map_or_else(
279        |_| password.as_bytes().to_vec(),
280        |normalised| normalised.into_owned().into_bytes(),
281    )
282}
283
284fn hi(password: &[u8], salt: &[u8], iterations: u32) -> [u8; 32] {
285    let mut input = Vec::with_capacity(salt.len() + 4);
286    input.extend_from_slice(salt);
287    input.extend_from_slice(&[0, 0, 0, 1]);
288    let mut previous = hmac(password, &input);
289    let mut output = previous;
290    for _ in 1..iterations {
291        previous = hmac(password, &previous);
292        for (output, previous) in output.iter_mut().zip(previous) {
293            *output ^= previous;
294        }
295    }
296    output
297}
298
299fn hmac(key: &[u8], input: &[u8]) -> [u8; 32] {
300    let mut mac = Hmac::<Sha256>::new_from_slice(key).expect("HMAC accepts every key length");
301    mac.update(input);
302    mac.finalize().into_bytes().into()
303}
304
305fn invalid(message: &'static str) -> io::Error {
306    io::Error::new(io::ErrorKind::InvalidInput, message)
307}
308
309#[cfg(test)]
310mod tests {
311    use postgres_protocol::authentication::sasl::{ChannelBinding, ScramSha256};
312
313    use super::*;
314
315    fn exchange(binding: ServerChannelBinding, client_binding: ChannelBinding, mechanism: &[u8]) {
316        let server = ScramServer::with_parameters(
317            b"pencil",
318            b"fixed test salt".to_vec(),
319            DEFAULT_ITERATIONS,
320            binding,
321        )
322        .unwrap();
323        let mut client = ScramSha256::new(b"pencil", client_binding);
324        let (exchange, server_first) = server.start(mechanism, client.message()).unwrap();
325        client.update(&server_first).unwrap();
326        let server_final = exchange.finish(client.message()).unwrap();
327        client.finish(&server_final).unwrap();
328    }
329
330    #[test]
331    fn verifies_scram_sha_256() {
332        exchange(
333            ServerChannelBinding::None,
334            ChannelBinding::unsupported(),
335            SCRAM_SHA_256,
336        );
337    }
338
339    #[test]
340    fn verifies_scram_sha_256_plus() {
341        let binding = b"certificate digest".to_vec();
342        exchange(
343            ServerChannelBinding::TlsServerEndPoint(binding.clone()),
344            ChannelBinding::tls_server_end_point(binding),
345            SCRAM_SHA_256_PLUS,
346        );
347    }
348
349    #[test]
350    fn rejects_wrong_password_and_channel_binding() {
351        let server = ScramServer::with_parameters(
352            b"correct",
353            b"fixed test salt".to_vec(),
354            DEFAULT_ITERATIONS,
355            ServerChannelBinding::None,
356        )
357        .unwrap();
358        let mut client = ScramSha256::new(b"wrong", ChannelBinding::unsupported());
359        let (exchange, server_first) = server.start(SCRAM_SHA_256, client.message()).unwrap();
360        client.update(&server_first).unwrap();
361        assert!(exchange.finish(client.message()).is_err());
362
363        let server = ScramServer::with_parameters(
364            b"correct",
365            b"fixed test salt".to_vec(),
366            DEFAULT_ITERATIONS,
367            ServerChannelBinding::TlsServerEndPoint(b"expected".to_vec()),
368        )
369        .unwrap();
370        let client = ScramSha256::new(
371            b"correct",
372            ChannelBinding::tls_server_end_point(b"different".to_vec()),
373        );
374        let (exchange, server_first) = server.start(SCRAM_SHA_256_PLUS, client.message()).unwrap();
375        let mut client = client;
376        client.update(&server_first).unwrap();
377        assert!(exchange.finish(client.message()).is_err());
378    }
379}