1use std::{fmt, num::ParseIntError, str::FromStr};
4
5mod client;
6mod messages;
7mod pg_verifier;
8mod server;
9
10pub use client::ScramClientExchange;
11pub use messages::{ScramClientFirst, TOKEN_AUTH_EXTENSION, decode_saslname, encode_saslname};
12pub use pg_verifier::{PgScramVerifier, ScramError};
13use refined_type::rule::MinMaxI32;
14use ring::{
15 hmac as ring_hmac,
16 rand::{SecureRandom, SystemRandom},
17};
18pub use server::{ScramServerExchange, StepResult};
19use sha2::{Digest, Sha256, Sha512};
20
21use crate::SaslMechanism;
22
23pub const DEFAULT_SCRAM_ITERATIONS: i32 = 8192;
25pub const MIN_SCRAM_ITERATIONS: i32 = 4096;
27pub const MAX_SCRAM_ITERATIONS: i32 = 16_384;
29
30#[derive(Clone, Copy, Debug, Eq, PartialEq)]
32pub struct ScramIterations(i32);
33
34impl ScramIterations {
35 pub fn new(value: i32) -> Result<Self, String> {
41 MinMaxI32::<MIN_SCRAM_ITERATIONS, MAX_SCRAM_ITERATIONS>::new(value)
42 .map(|value| Self(value.into_value()))
43 .map_err(|error| error.to_string())
44 }
45
46 #[must_use]
47 pub const fn into_value(self) -> i32 {
48 self.0
49 }
50}
51
52impl Default for ScramIterations {
53 fn default() -> Self {
54 Self(DEFAULT_SCRAM_ITERATIONS)
56 }
57}
58
59impl fmt::Display for ScramIterations {
60 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
61 self.0.fmt(formatter)
62 }
63}
64
65impl FromStr for ScramIterations {
66 type Err = String;
67
68 fn from_str(value: &str) -> Result<Self, Self::Err> {
69 value
70 .parse()
71 .map_err(|error: ParseIntError| error.to_string())
72 .and_then(Self::new)
73 }
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct ScramCredential {
78 pub mechanism: SaslMechanism,
79 pub salt: Vec<u8>,
80 pub stored_key: Vec<u8>,
81 pub server_key: Vec<u8>,
82 pub iterations: u32,
83}
84
85#[must_use]
90pub fn scram_hash_len(mechanism: SaslMechanism) -> usize {
93 match mechanism {
94 SaslMechanism::ScramSha256 => 32,
95 SaslMechanism::ScramSha512 => 64,
96 SaslMechanism::Plain | SaslMechanism::OAuthBearer | SaslMechanism::Gssapi => {
97 panic!("scram_hash_len called with non-SCRAM mechanism {mechanism:?}")
98 }
99 }
100}
101
102pub fn hash_scram_password(
113 password: &[u8],
114 mechanism: SaslMechanism,
115 iterations: u32,
116) -> Result<ScramCredential, ScramError> {
117 assert2::assert!(iterations > 0);
118 let mut salt = vec![0u8; 16];
119 SystemRandom::new()
120 .fill(&mut salt)
121 .map_err(|_| ScramError::RandomUnavailable)?;
122 Ok(hash_scram_password_with_salt(
123 password, mechanism, iterations, salt,
124 ))
125}
126
127#[must_use]
129pub fn hash_scram_password_with_salt(
132 password: &[u8],
133 mechanism: SaslMechanism,
134 iterations: u32,
135 salt: Vec<u8>,
136) -> ScramCredential {
137 let (stored_key, server_key) = match mechanism {
138 SaslMechanism::ScramSha512 => {
139 let salted: [u8; 64] =
140 pbkdf2::pbkdf2_hmac_array::<Sha512, 64>(password, &salt, iterations);
141 derive_keys_sha512(&salted)
142 }
143 SaslMechanism::ScramSha256 => {
144 let salted: [u8; 32] =
145 pbkdf2::pbkdf2_hmac_array::<Sha256, 32>(password, &salt, iterations);
146 derive_keys_sha256(&salted)
147 }
148 SaslMechanism::Plain | SaslMechanism::OAuthBearer | SaslMechanism::Gssapi => {
149 panic!("hash_scram_password called with non-SCRAM mechanism {mechanism:?}");
150 }
151 };
152 ScramCredential {
153 mechanism,
154 salt,
155 stored_key,
156 server_key,
157 iterations,
158 }
159}
160
161#[must_use]
172pub fn pbkdf2_salted(
175 password: &[u8],
176 mechanism: SaslMechanism,
177 iterations: u32,
178 salt: &[u8],
179) -> Vec<u8> {
180 assert2::assert!(iterations > 0);
181 match mechanism {
182 SaslMechanism::ScramSha512 => {
183 let arr: [u8; 64] = pbkdf2::pbkdf2_hmac_array::<Sha512, 64>(password, salt, iterations);
184 arr.to_vec()
185 }
186 SaslMechanism::ScramSha256 => {
187 let arr: [u8; 32] = pbkdf2::pbkdf2_hmac_array::<Sha256, 32>(password, salt, iterations);
188 arr.to_vec()
189 }
190 SaslMechanism::Plain | SaslMechanism::OAuthBearer | SaslMechanism::Gssapi => {
191 panic!("pbkdf2_salted called with non-SCRAM mechanism {mechanism:?}");
192 }
193 }
194}
195
196#[must_use]
215pub fn derive_keys_from_salted(mechanism: SaslMechanism, salted: &[u8]) -> (Vec<u8>, Vec<u8>) {
218 match mechanism {
219 SaslMechanism::ScramSha512 => derive_keys_sha512(salted),
220 SaslMechanism::ScramSha256 => derive_keys_sha256(salted),
221 SaslMechanism::Plain | SaslMechanism::OAuthBearer | SaslMechanism::Gssapi => {
222 panic!("derive_keys_from_salted called with non-SCRAM mechanism {mechanism:?}");
223 }
224 }
225}
226
227fn derive_keys_sha512(salted: &[u8]) -> (Vec<u8>, Vec<u8>) {
228 derive_keys_with(ring_hmac::HMAC_SHA512, salted, |client_key| {
229 Sha512::digest(client_key).to_vec()
230 })
231}
232
233fn derive_keys_sha256(salted: &[u8]) -> (Vec<u8>, Vec<u8>) {
234 derive_keys_with(ring_hmac::HMAC_SHA256, salted, |client_key| {
235 Sha256::digest(client_key).to_vec()
236 })
237}
238
239fn derive_keys_with(
245 algorithm: ring_hmac::Algorithm,
246 salted: &[u8],
247 hash: impl FnOnce(&[u8]) -> Vec<u8>,
248) -> (Vec<u8>, Vec<u8>) {
249 let key = ring_hmac::Key::new(algorithm, salted);
250 let client_key = ring_hmac::sign(&key, b"Client Key");
251 let stored_key = hash(client_key.as_ref());
252 let server_key = ring_hmac::sign(&key, b"Server Key").as_ref().to_vec();
253 (stored_key, server_key)
254}
255
256#[cfg(test)]
257mod tests {
258 use assert2::check;
259 use base64::{Engine, engine::general_purpose::STANDARD as B64};
260 use hmac::{Hmac, KeyInit, Mac};
261 use sha2::{Digest, Sha512};
262
263 use super::*;
264 use crate::{AuthMethod, Principal};
265
266 #[test]
273 fn scram_iterations_round_trip_through_value_text_and_parse() {
274 let iterations = ScramIterations::new(MIN_SCRAM_ITERATIONS + 1).expect("in range");
275 check!(iterations.into_value() == MIN_SCRAM_ITERATIONS + 1);
276 check!(iterations.to_string() == (MIN_SCRAM_ITERATIONS + 1).to_string());
277 check!(
278 (MIN_SCRAM_ITERATIONS + 1)
279 .to_string()
280 .parse::<ScramIterations>()
281 == Ok(iterations)
282 );
283
284 check!(ScramIterations::default().into_value() == DEFAULT_SCRAM_ITERATIONS);
285 check!(
286 ScramIterations::new(MAX_SCRAM_ITERATIONS)
287 .expect("the maximum is in range")
288 .into_value()
289 == MAX_SCRAM_ITERATIONS
290 );
291
292 check!(ScramIterations::new(MIN_SCRAM_ITERATIONS - 1).is_err());
294 check!(ScramIterations::new(MAX_SCRAM_ITERATIONS + 1).is_err());
295 check!("not-a-number".parse::<ScramIterations>().is_err());
296 }
297
298 #[test]
299 fn hash_scram_password_produces_expected_keys() {
300 let password = b"pencil";
301 let cred = hash_scram_password(password, SaslMechanism::ScramSha512, 4096).unwrap();
302 check!(
303 (
304 cred.mechanism,
305 cred.salt.len(),
306 cred.stored_key.len(),
307 cred.server_key.len(),
308 cred.iterations,
309 ) == (SaslMechanism::ScramSha512, 16, 64, 64, 4096)
310 );
311 let salted =
312 pbkdf2::pbkdf2_hmac_array::<sha2::Sha512, 64>(password, &cred.salt, cred.iterations);
313 let client_key = {
314 use hmac::{Hmac, KeyInit, Mac};
315 let mut m = <Hmac<Sha512>>::new_from_slice(&salted).unwrap();
316 m.update(b"Client Key");
317 m.finalize().into_bytes()
318 };
319 let expected_stored = Sha512::digest(client_key);
320 assert2::assert!(cred.stored_key == expected_stored.as_slice());
321 }
322
323 #[test]
326 fn hash_scram_password_sha256_produces_expected_keys() {
327 let password = b"pencil";
328 let cred = hash_scram_password(password, SaslMechanism::ScramSha256, 4096).unwrap();
329 check!(
330 (
331 cred.mechanism,
332 cred.salt.len(),
333 cred.stored_key.len(),
334 cred.server_key.len(),
335 ) == (SaslMechanism::ScramSha256, 16, 32, 32)
336 );
337 let salted =
338 pbkdf2::pbkdf2_hmac_array::<sha2::Sha256, 32>(password, &cred.salt, cred.iterations);
339 let client_key = {
340 use hmac::{Hmac, KeyInit, Mac};
341 let mut m = <Hmac<sha2::Sha256>>::new_from_slice(&salted).unwrap();
342 m.update(b"Client Key");
343 m.finalize().into_bytes()
344 };
345 let expected_stored = sha2::Sha256::digest(client_key);
346 assert2::assert!(cred.stored_key == expected_stored.as_slice());
347 }
348
349 #[test]
350 fn hash_scram_password_is_deterministic_given_salt() {
351 let a = hash_scram_password(b"x", SaslMechanism::ScramSha512, 4096).unwrap();
352 let b = hash_scram_password(b"x", SaslMechanism::ScramSha512, 4096).unwrap();
353 assert2::assert!(a.salt != b.salt);
354 }
355
356 use crate::{
357 AuthError,
358 scram::{
359 client::ScramClientExchange,
360 server::{ScramServerExchange, StepResult},
361 },
362 };
363
364 #[test]
365 fn scram_server_and_client_round_trip() {
366 let password = b"hunter2";
367 let cred = hash_scram_password_with_salt(
368 password,
369 SaslMechanism::ScramSha512,
370 4096,
371 (0..16).collect::<Vec<u8>>(),
372 );
373 let server = ScramServerExchange::new("alice".to_owned(), cred);
374 let client = ScramClientExchange::new(
375 "alice".to_owned(),
376 password.to_vec(),
377 SaslMechanism::ScramSha512,
378 );
379
380 let (c1, client) = client.client_first().expect("client first");
382 let (s1, server) = match server.step(&c1) {
384 StepResult::Continue(b, next) => (b, next),
385 other => panic!("server step 1 must continue, got {other:?}"),
386 };
387 let (c2, client) = client.step(&s1).expect("client final");
389 let (principal, s2) = match server.step(&c2) {
391 StepResult::Done(p, b) => (p, b),
392 other => panic!("server step 2 must Done, got {other:?}"),
393 };
394 assert2::assert!(principal.name.as_str() == "alice");
395 assert2::assert!(principal.auth_method == crate::AuthMethod::SaslScramSha512);
396 let final_check = client.verify_server_final(&s2);
402 assert2::assert!(final_check.is_ok());
403 }
404
405 #[test]
409 fn scram_server_and_client_round_trip_sha256() {
410 let password = b"hunter2";
411 let cred = hash_scram_password_with_salt(
412 password,
413 SaslMechanism::ScramSha256,
414 4096,
415 (0..16).collect::<Vec<u8>>(),
416 );
417 let server = ScramServerExchange::new("alice".to_owned(), cred);
418 let client = ScramClientExchange::new(
419 "alice".to_owned(),
420 password.to_vec(),
421 SaslMechanism::ScramSha256,
422 );
423
424 let (c1, client) = client.client_first().expect("client first");
425 let (s1, server) = match server.step(&c1) {
426 StepResult::Continue(b, next) => (b, next),
427 other => panic!("server step 1 must continue, got {other:?}"),
428 };
429 let (c2, client) = client.step(&s1).expect("client final");
430 let (principal, s2) = match server.step(&c2) {
431 StepResult::Done(p, b) => (p, b),
432 other => panic!("server step 2 must Done, got {other:?}"),
433 };
434 assert2::assert!(principal.name.as_str() == "alice");
435 assert2::assert!(principal.auth_method == crate::AuthMethod::SaslScramSha256);
436 let final_check = client.verify_server_final(&s2);
437 assert2::assert!(final_check.is_ok());
438 }
439
440 #[test]
444 fn scram_server_exchange_debug_includes_the_observable_phase() {
445 let password = b"hunter2";
446 let cred = hash_scram_password_with_salt(
447 password,
448 SaslMechanism::ScramSha256,
449 4096,
450 (0..16).collect::<Vec<u8>>(),
451 );
452 let server = ScramServerExchange::new("alice".to_owned(), cred);
453 let rendered = format!("{server:?}");
454 assert2::assert!(rendered.contains("ScramServerExchange::AwaitingClientFirst"));
455
456 let client = ScramClientExchange::new(
457 "alice".to_owned(),
458 password.to_vec(),
459 SaslMechanism::ScramSha256,
460 );
461 let (c1, _client) = client.client_first().expect("client first");
462 let server = match server.step(&c1) {
463 StepResult::Continue(_, next) => next,
464 other => panic!("server step 1 must continue, got {other:?}"),
465 };
466 let rendered = format!("{server:?}");
467 assert2::assert!(rendered.contains("ScramServerExchange::AwaitingClientFinal"));
468 }
469
470 #[test]
471 fn scram_client_rejects_tampered_server_final_signature() {
472 let password = b"hunter2";
473 let cred = hash_scram_password_with_salt(
474 password,
475 SaslMechanism::ScramSha256,
476 4096,
477 (0..16).collect::<Vec<u8>>(),
478 );
479 let server = ScramServerExchange::new("alice".to_owned(), cred);
480 let client = ScramClientExchange::new(
481 "alice".to_owned(),
482 password.to_vec(),
483 SaslMechanism::ScramSha256,
484 );
485
486 let (c1, client) = client.client_first().expect("client first");
487 let (s1, server) = match server.step(&c1) {
488 StepResult::Continue(b, next) => (b, next),
489 other => panic!("server step 1 must continue, got {other:?}"),
490 };
491 let (c2, client) = client.step(&s1).expect("client final");
492 match server.step(&c2) {
493 StepResult::Done(_, _) => {}
494 other => panic!("server step 2 must Done, got {other:?}"),
495 }
496
497 assert2::assert!(client.verify_server_final(b"v=AAAA").is_err());
498 }
499
500 #[derive(Debug, PartialEq, Eq)]
502 enum Ending {
503 Done(Principal, Vec<u8>),
504 Failed(AuthError),
505 Disconnect,
506 }
507
508 impl TryFrom<StepResult> for Ending {
511 type Error = StepResult;
512
513 fn try_from(result: StepResult) -> Result<Self, StepResult> {
514 match result {
515 StepResult::Done(principal, server_final) => {
516 Ok(Self::Done(principal, server_final))
517 }
518 StepResult::Failed(e) => Ok(Self::Failed(e)),
519 StepResult::Disconnect => Ok(Self::Disconnect),
520 continued @ StepResult::Continue(..) => Err(continued),
521 }
522 }
523 }
524
525 #[test]
530 fn scram_server_disconnects_on_a_proof_of_the_wrong_length() {
531 let password = b"hunter2";
532 for (case, mechanism, proof_len, wrong_nonce, want) in [
533 (
534 "empty proof",
535 SaslMechanism::ScramSha256,
536 0,
537 false,
538 Ending::Disconnect,
539 ),
540 (
541 "short proof",
542 SaslMechanism::ScramSha256,
543 3,
544 false,
545 Ending::Disconnect,
546 ),
547 (
548 "one byte short",
549 SaslMechanism::ScramSha256,
550 31,
551 false,
552 Ending::Disconnect,
553 ),
554 (
555 "one byte long",
556 SaslMechanism::ScramSha256,
557 33,
558 false,
559 Ending::Disconnect,
560 ),
561 (
562 "SHA-512 length on SHA-256",
563 SaslMechanism::ScramSha256,
564 64,
565 false,
566 Ending::Disconnect,
567 ),
568 (
569 "SHA-256 length on SHA-512",
570 SaslMechanism::ScramSha512,
571 32,
572 false,
573 Ending::Disconnect,
574 ),
575 (
576 "right length, wrong value",
577 SaslMechanism::ScramSha256,
578 32,
579 false,
580 Ending::Failed(AuthError::BadProof),
581 ),
582 (
583 "wrong length and wrong nonce",
584 SaslMechanism::ScramSha256,
585 3,
586 true,
587 Ending::Failed(AuthError::MalformedMessage),
588 ),
589 ] {
590 let cred = hash_scram_password_with_salt(
591 password,
592 mechanism,
593 4096,
594 (0..16).collect::<Vec<u8>>(),
595 );
596 let server = ScramServerExchange::new("alice".to_owned(), cred);
597 let StepResult::Continue(server_first, server) = server.step(b"n,,n=alice,r=cn") else {
598 panic!("server step 1 must continue");
599 };
600 let server_first = String::from_utf8(server_first).unwrap();
601 let nonce = server_first
602 .strip_prefix("r=")
603 .and_then(|rest| rest.split(',').next())
604 .unwrap();
605 let nonce = if wrong_nonce { "cnX" } else { nonce };
606 let client_final = format!("c=biws,r={nonce},p={}", B64.encode(vec![7u8; proof_len]));
607 check!(
608 Ending::try_from(server.step(client_final.as_bytes())).unwrap() == want,
609 "case {case}"
610 );
611 }
612 }
613
614 #[test]
615 fn pbkdf2_salted_matches_hash_scram_password_intermediate() {
616 let password = b"pencil";
621 let salt: Vec<u8> = (0..16).collect();
622 for (_case, mechanism, expected_len) in [
623 ("SCRAM SHA-512", SaslMechanism::ScramSha512, 64),
624 ("SCRAM SHA-256", SaslMechanism::ScramSha256, 32),
625 ] {
626 let cred = hash_scram_password_with_salt(password, mechanism, 4096, salt.clone());
627 let salted = pbkdf2_salted(password, mechanism, 4096, &salt);
628 let (stored_key, server_key) = derive_keys_from_salted(mechanism, &salted);
629 assert2::assert!(salted.len() == expected_len);
630 assert2::assert!(stored_key == cred.stored_key);
631 assert2::assert!(server_key == cred.server_key);
632 }
633 }
634
635 #[test]
636 fn derive_keys_from_salted_matches_hash_scram_password_sha512() {
637 let password = b"hunter2";
638 let salt: Vec<u8> = (0..16).collect();
639 let cred =
640 hash_scram_password_with_salt(password, SaslMechanism::ScramSha512, 4096, salt.clone());
641 let salted: [u8; 64] = pbkdf2::pbkdf2_hmac_array::<sha2::Sha512, 64>(password, &salt, 4096);
642 let (stored_key, server_key) = derive_keys_from_salted(SaslMechanism::ScramSha512, &salted);
643 check!(
644 (&stored_key, &server_key, stored_key.len(), server_key.len())
645 == (&cred.stored_key, &cred.server_key, 64, 64)
646 );
647 }
648
649 #[test]
650 fn derive_keys_from_salted_matches_hash_scram_password_sha256() {
651 let password = b"hunter2";
652 let salt: Vec<u8> = (0..16).collect();
653 let cred =
654 hash_scram_password_with_salt(password, SaslMechanism::ScramSha256, 4096, salt.clone());
655 let salted: [u8; 32] = pbkdf2::pbkdf2_hmac_array::<sha2::Sha256, 32>(password, &salt, 4096);
656 let (stored_key, server_key) = derive_keys_from_salted(SaslMechanism::ScramSha256, &salted);
657 check!(
658 (&stored_key, &server_key, stored_key.len(), server_key.len())
659 == (&cred.stored_key, &cred.server_key, 32, 32)
660 );
661 }
662
663 #[test]
669 fn scram_server_with_principal_override_yields_override_on_done() {
670 let password = b"hunter2";
671 let cred = hash_scram_password_with_salt(
672 password,
673 SaslMechanism::ScramSha256,
674 4096,
675 (0..16).collect::<Vec<u8>>(),
676 );
677 let override_principal = crate::Principal {
678 name: "alice".to_owned(),
679 auth_method: crate::AuthMethod::SaslScramSha256,
680 groups: vec![],
681 };
682 let server = ScramServerExchange::new_with_principal(
685 "tok-uuid".to_owned(),
686 cred,
687 override_principal.clone(),
688 );
689 let client = ScramClientExchange::new(
690 "tok-uuid".to_owned(),
691 password.to_vec(),
692 SaslMechanism::ScramSha256,
693 );
694
695 let (c1, client) = client.client_first().expect("client first");
696 let (s1, server) = match server.step(&c1) {
697 StepResult::Continue(b, next) => (b, next),
698 other => panic!("server step 1 must continue, got {other:?}"),
699 };
700 let (c2, _client) = client.step(&s1).expect("client final");
701 let (principal, _s2) = match server.step(&c2) {
702 StepResult::Done(p, b) => (p, b),
703 other => panic!("server step 2 must Done, got {other:?}"),
704 };
705 assert2::assert!(principal == override_principal);
707 }
708
709 #[test]
710 fn scram_server_rejects_bad_proof() {
711 let cred = hash_scram_password_with_salt(
712 b"correct",
713 SaslMechanism::ScramSha512,
714 4096,
715 vec![0u8; 16],
716 );
717 let server = ScramServerExchange::new("alice".to_owned(), cred);
718 let client = ScramClientExchange::new(
719 "alice".to_owned(),
720 b"wrong".to_vec(),
721 SaslMechanism::ScramSha512,
722 );
723 let (c1, client) = client.client_first().unwrap();
724 let StepResult::Continue(s1, server) = server.step(&c1) else {
725 panic!();
726 };
727 let (c2, _client) = client.step(&s1).unwrap();
728 match server.step(&c2) {
729 StepResult::Failed(crate::AuthError::BadProof) => {}
730 other => panic!("expected BadProof, got {other:?}"),
731 }
732 }
733
734 #[test]
739 fn scram_server_rejects_wrong_nonce() {
740 let password: Vec<u8> = (b'A'..=b'Z').collect();
743 let cred = hash_scram_password_with_salt(
744 &password,
745 SaslMechanism::ScramSha256,
746 4096,
747 (0..16).collect::<Vec<u8>>(),
748 );
749 let server = ScramServerExchange::new("alice".to_owned(), cred);
750 let client =
751 ScramClientExchange::new("alice".to_owned(), password, SaslMechanism::ScramSha256);
752 let (c1, client) = client.client_first().unwrap();
753 let StepResult::Continue(s1, server) = server.step(&c1) else {
754 panic!("server step 1 must continue");
755 };
756 let (c2, _client) = client.step(&s1).unwrap();
757 let c2_str = String::from_utf8(c2).unwrap();
758 let combined = c2_str
761 .split(',')
762 .find_map(|a| a.strip_prefix("r="))
763 .expect("client-final has r=");
764 let tampered = c2_str.replacen(
765 &format!("r={combined}"),
766 &format!("r={combined}deadbeef"),
767 1,
768 );
769 match server.step(tampered.as_bytes()) {
770 StepResult::Failed(crate::AuthError::MalformedMessage) => {}
771 other => panic!("expected MalformedMessage for wrong nonce, got {other:?}"),
772 }
773 }
774
775 #[test]
779 fn scram_server_fails_a_channel_binding_the_client_did_not_sign() {
780 let password: Vec<u8> = (b'A'..=b'Z').collect();
783 let cred = hash_scram_password_with_salt(
784 &password,
785 SaslMechanism::ScramSha256,
786 4096,
787 (0..16).collect::<Vec<u8>>(),
788 );
789 let server = ScramServerExchange::new("alice".to_owned(), cred);
790 let client =
791 ScramClientExchange::new("alice".to_owned(), password, SaslMechanism::ScramSha256);
792 let (c1, client) = client.client_first().unwrap();
793 let StepResult::Continue(s1, server) = server.step(&c1) else {
794 panic!("server step 1 must continue");
795 };
796 let (c2, _client) = client.step(&s1).unwrap();
797 let c2_str = String::from_utf8(c2).unwrap();
798 assert2::assert!(c2_str.starts_with("c=biws,"));
801 let wrong_cb = B64.encode(b"y,,");
802 let tampered = c2_str.replacen("c=biws", &format!("c={wrong_cb}"), 1);
803 match server.step(tampered.as_bytes()) {
804 StepResult::Failed(crate::AuthError::BadProof) => {}
805 other => panic!("expected BadProof for an unsigned channel binding, got {other:?}"),
806 }
807 }
808
809 fn handshake_with_header(
814 gs2_header: &str,
815 extensions: &str,
816 channel_binding_header: &str,
817 ) -> Result<crate::Principal, crate::AuthError> {
818 let password: Vec<u8> = (b'a'..=b'z').collect();
819 let salt: Vec<u8> = (0..16).collect();
820 let cred = hash_scram_password_with_salt(
821 &password,
822 SaslMechanism::ScramSha256,
823 4096,
824 salt.clone(),
825 );
826 let server = ScramServerExchange::new("alice".to_owned(), cred);
827
828 let bare = format!("n=alice,r=client-nonce{extensions}");
829 let (server_first, server) = match server.step(format!("{gs2_header}{bare}").as_bytes()) {
830 StepResult::Continue(server_first, next) => (server_first, next),
831 StepResult::Failed(e) => return Err(e),
832 StepResult::Done(..) | StepResult::Disconnect => {
833 panic!("server ended the exchange on client-first")
834 }
835 };
836 let server_first = String::from_utf8(server_first).unwrap();
837 let combined_nonce = server_first
838 .strip_prefix("r=")
839 .and_then(|rest| rest.split(',').next())
840 .unwrap();
841
842 let client_final_no_proof = format!(
843 "c={},r={combined_nonce}",
844 B64.encode(channel_binding_header.as_bytes())
845 );
846 let auth_message = format!("{bare},{server_first},{client_final_no_proof}");
847 let (proof, _) =
848 client::compute_proof_sha256(&password, &salt, 4096, auth_message.as_bytes()).unwrap();
849 match server.step(format!("{client_final_no_proof},p={}", B64.encode(proof)).as_bytes()) {
850 StepResult::Done(principal, _) => Ok(principal),
851 StepResult::Failed(e) => Err(e),
852 StepResult::Continue(..) | StepResult::Disconnect => {
853 panic!("server did not finish client-final")
854 }
855 }
856 }
857
858 #[test]
863 fn scram_server_reads_the_gs2_header_but_not_the_channel_binding() {
864 let alice = Ok(crate::Principal {
865 name: "alice".into(),
866 auth_method: crate::AuthMethod::SaslScramSha256,
867 groups: vec![],
868 });
869 for (case, gs2_header, extensions, channel_binding_header, want) in [
870 ("plain header", "n,,", "", "n,,", alice.clone()),
871 (
872 "authzid naming the user",
873 "n,a=alice,",
874 "",
875 "n,a=alice,",
876 alice.clone(),
877 ),
878 (
879 "token extension",
880 "n,,",
881 ",tokenauth=true",
882 "n,,",
883 alice.clone(),
884 ),
885 (
886 "authzid with extension",
887 "n,a=alice,",
888 ",tokenauth=true",
889 "n,a=alice,",
890 alice.clone(),
891 ),
892 (
893 "authzid naming another user",
894 "n,a=bob,",
895 "",
896 "n,a=bob,",
897 Err(crate::AuthError::AuthorizationIdMismatch),
898 ),
899 (
900 "authzid header but biws binding",
901 "n,a=alice,",
902 "",
903 "n,,",
904 alice.clone(),
905 ),
906 (
907 "plain header but authzid binding",
908 "n,,",
909 "",
910 "n,a=alice,",
911 alice,
912 ),
913 (
914 "channel binding requested",
915 "p=tls-unique,,",
916 "",
917 "p=tls-unique,,",
918 Err(crate::AuthError::MalformedMessage),
919 ),
920 (
921 "client supports binding",
922 "y,,",
923 "",
924 "y,,",
925 Err(crate::AuthError::MalformedMessage),
926 ),
927 ] {
928 check!(
929 handshake_with_header(gs2_header, extensions, channel_binding_header) == want,
930 "case {case}"
931 );
932 }
933 }
934
935 fn handshake_with_client_final(
944 username: &str,
945 gs2_header: &str,
946 bare: &str,
947 template: &str,
948 flip_unused_bit: bool,
949 ) -> Result<Principal, AuthError> {
950 let password: Vec<u8> = (b'a'..=b'z').collect();
951 let salt: Vec<u8> = (0..16).collect();
952 let cred = hash_scram_password_with_salt(
953 &password,
954 SaslMechanism::ScramSha256,
955 4096,
956 salt.clone(),
957 );
958 let server = ScramServerExchange::new(username.to_owned(), cred);
959 let (server_first, server) = match server.step(format!("{gs2_header}{bare}").as_bytes()) {
960 StepResult::Continue(server_first, next) => (server_first, next),
961 StepResult::Failed(e) => return Err(e),
962 StepResult::Done(..) | StepResult::Disconnect => {
963 panic!("server ended the exchange on client-first")
964 }
965 };
966 let server_first = String::from_utf8(server_first).unwrap();
967 let combined_nonce = server_first
968 .strip_prefix("r=")
969 .and_then(|rest| rest.split(',').next())
970 .unwrap();
971 let client_final = template
972 .replace("{cb}", &B64.encode(gs2_header.as_bytes()))
973 .replace("{nonce}", combined_nonce);
974 let signed = format!("c={},r={combined_nonce}", B64.encode(gs2_header.as_bytes()));
975 let auth_message = format!("{bare},{server_first},{signed}");
976 let (client_proof, _) =
977 client::compute_proof_sha256(&password, &salt, 4096, auth_message.as_bytes()).unwrap();
978 let mut proof_b64 = B64.encode(client_proof);
979 if flip_unused_bit {
980 let alphabet = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
983 let last = proof_b64.as_bytes()[42];
984 let index = alphabet.iter().position(|&c| c == last).unwrap();
985 proof_b64.replace_range(42..43, &char::from(alphabet[index ^ 1]).to_string());
986 }
987 match server.step(client_final.replace("{proof}", &proof_b64).as_bytes()) {
988 StepResult::Done(principal, _) => Ok(principal),
989 StepResult::Failed(e) => Err(e),
990 StepResult::Continue(..) | StepResult::Disconnect => {
991 panic!("server did not finish client-final")
992 }
993 }
994 }
995
996 #[test]
1001 fn scram_server_reads_client_final_with_kafkas_grammar() {
1002 let principal = |name: &str| {
1003 Ok(Principal {
1004 name: name.into(),
1005 auth_method: AuthMethod::SaslScramSha256,
1006 groups: vec![],
1007 })
1008 };
1009 let malformed = Err(AuthError::MalformedMessage);
1010 let alice_bare = "n=alice,r=client-nonce";
1011 for (case, username, gs2_header, bare, template, flip_unused_bit, want) in [
1012 (
1013 "kafka order",
1014 "alice",
1015 "n,,",
1016 alice_bare,
1017 "c={cb},r={nonce},p={proof}",
1018 false,
1019 principal("alice"),
1020 ),
1021 (
1022 "extension before the proof is not signed",
1023 "alice",
1024 "n,,",
1025 alice_bare,
1026 "c={cb},r={nonce},foo=bar,p={proof}",
1027 false,
1028 principal("alice"),
1029 ),
1030 (
1031 "unused proof bit set",
1032 "alice",
1033 "n,,",
1034 alice_bare,
1035 "c={cb},r={nonce},p={proof}",
1036 true,
1037 principal("alice"),
1038 ),
1039 (
1040 "nonce before channel binding",
1041 "alice",
1042 "n,,",
1043 alice_bare,
1044 "r={nonce},c={cb},p={proof}",
1045 false,
1046 malformed.clone(),
1047 ),
1048 (
1049 "proof before nonce",
1050 "alice",
1051 "n,,",
1052 alice_bare,
1053 "c={cb},p={proof},r={nonce}",
1054 false,
1055 malformed.clone(),
1056 ),
1057 (
1058 "unknown attribute after the proof",
1059 "alice",
1060 "n,,",
1061 alice_bare,
1062 "c={cb},r={nonce},p={proof},x=1",
1063 false,
1064 malformed.clone(),
1065 ),
1066 (
1067 "duplicate channel binding",
1068 "alice",
1069 "n,,",
1070 alice_bare,
1071 "c={cb},c={cb},r={nonce},p={proof}",
1072 false,
1073 malformed.clone(),
1074 ),
1075 (
1076 "repeated nonce",
1078 "alice",
1079 "n,,",
1080 alice_bare,
1081 "c={cb},r={nonce},r={nonce},p={proof}",
1082 false,
1083 principal("alice"),
1084 ),
1085 (
1086 "trailing comma",
1087 "alice",
1088 "n,,",
1089 alice_bare,
1090 "c={cb},r={nonce},p={proof},",
1091 false,
1092 malformed,
1093 ),
1094 (
1095 "escaped authzid equal to the decoded username",
1096 "a=3D",
1097 "n,a=a=3D,",
1098 "n=a=3D3D,r=client-nonce",
1099 "c={cb},r={nonce},p={proof}",
1100 false,
1101 principal("a=3D"),
1102 ),
1103 (
1104 "escaped authzid that decodes to the username",
1105 "a,b",
1106 "n,a=a=2Cb,",
1107 "n=a=2Cb,r=client-nonce",
1108 "c={cb},r={nonce},p={proof}",
1109 false,
1110 Err(AuthError::AuthorizationIdMismatch),
1111 ),
1112 ] {
1113 check!(
1114 handshake_with_client_final(username, gs2_header, bare, template, flip_unused_bit)
1115 == want,
1116 "case {case}"
1117 );
1118 }
1119 }
1120
1121 #[test]
1130 fn scram_server_signs_the_auth_message_kafka_rebuilds() {
1131 let password: Vec<u8> = (b'a'..=b'z').collect();
1132 let salt: Vec<u8> = (0..16).collect();
1133 let alice = Principal {
1134 name: "alice".into(),
1135 auth_method: AuthMethod::SaslScramSha256,
1136 groups: vec![],
1137 };
1138 for (case, client_first, client_final, kafka_bare, kafka_final) in [
1139 (
1140 "canonical",
1141 "n,,n=alice,r=cn",
1142 "c=biws,r={nonce},p={proof}",
1143 "n=alice,r=cn",
1144 "c=biws,r={nonce}",
1145 ),
1146 (
1147 "reserved attribute",
1148 "n,,m=ext,n=alice,r=cn",
1149 "c=biws,r={nonce},p={proof}",
1150 "n=alice,r=cn",
1151 "c=biws,r={nonce}",
1152 ),
1153 (
1154 "repeated client-first extension",
1155 "n,,n=alice,r=cn,foo=1,foo=2",
1156 "c=biws,r={nonce},p={proof}",
1157 "n=alice,r=cn,foo=2",
1158 "c=biws,r={nonce}",
1159 ),
1160 (
1161 "client-final extension",
1162 "n,,n=alice,r=cn",
1163 "c=biws,r={nonce},foo=bar,p={proof}",
1164 "n=alice,r=cn",
1165 "c=biws,r={nonce}",
1166 ),
1167 (
1168 "unused bits in the channel binding",
1169 "n,a=alice,n=alice,r=cn",
1170 "c=bixhPWFsaWNlLB==,r={nonce},p={proof}",
1171 "n=alice,r=cn",
1172 "c=bixhPWFsaWNlLA==,r={nonce}",
1173 ),
1174 (
1175 "channel binding of another header",
1176 "n,,n=alice,r=cn",
1177 "c=bixhPWFsaWNlLA==,r={nonce},p={proof}",
1178 "n=alice,r=cn",
1179 "c=bixhPWFsaWNlLA==,r={nonce}",
1180 ),
1181 ] {
1182 let raw_bare = client_first.splitn(3, ',').nth(2).unwrap();
1183 let raw_final = client_final.split(",p={proof}").next().unwrap();
1184 for (signed_bare, signed_final) in [(kafka_bare, kafka_final), (raw_bare, raw_final)] {
1185 let cred = hash_scram_password_with_salt(
1186 &password,
1187 SaslMechanism::ScramSha256,
1188 4096,
1189 salt.clone(),
1190 );
1191 let server = ScramServerExchange::new("alice".to_owned(), cred);
1192 let StepResult::Continue(server_first, server) =
1193 server.step(client_first.as_bytes())
1194 else {
1195 panic!("case {case}: server step 1 must continue");
1196 };
1197 let server_first = String::from_utf8(server_first).unwrap();
1198 let combined_nonce = server_first
1199 .strip_prefix("r=")
1200 .and_then(|rest| rest.split(',').next())
1201 .unwrap();
1202 let auth_message = format!(
1203 "{signed_bare},{server_first},{}",
1204 signed_final.replace("{nonce}", combined_nonce)
1205 );
1206 let (client_proof, server_key) =
1207 client::compute_proof_sha256(&password, &salt, 4096, auth_message.as_bytes())
1208 .unwrap();
1209 let mut server_mac = <Hmac<Sha256>>::new_from_slice(&server_key).unwrap();
1210 server_mac.update(auth_message.as_bytes());
1211 let server_final =
1212 format!("v={}", B64.encode(server_mac.finalize().into_bytes())).into_bytes();
1213 let want = if signed_bare == kafka_bare && signed_final == kafka_final {
1214 Ending::Done(alice.clone(), server_final)
1215 } else {
1216 Ending::Failed(AuthError::BadProof)
1217 };
1218 let message = client_final
1219 .replace("{nonce}", combined_nonce)
1220 .replace("{proof}", &B64.encode(client_proof));
1221 check!(
1222 Ending::try_from(server.step(message.as_bytes())).unwrap() == want,
1223 "case {case}, signed {signed_bare} / {signed_final}"
1224 );
1225 }
1226 }
1227 }
1228
1229 #[test]
1233 fn scram_client_sends_extensions_in_client_first() {
1234 let password = b"token-hmac";
1235 let cred = hash_scram_password_with_salt(
1236 password,
1237 SaslMechanism::ScramSha256,
1238 4096,
1239 (0..16).collect::<Vec<u8>>(),
1240 );
1241 let server = ScramServerExchange::new("token-id".to_owned(), cred);
1242 let client = ScramClientExchange::new(
1243 "token-id".to_owned(),
1244 password.to_vec(),
1245 SaslMechanism::ScramSha256,
1246 )
1247 .with_extensions([(TOKEN_AUTH_EXTENSION, "true")])
1248 .unwrap();
1249
1250 let (c1, client) = client.client_first().unwrap();
1251 let parsed = ScramClientFirst::parse(&c1).unwrap();
1252 check!(
1253 (
1254 parsed.gs2_header.as_str(),
1255 parsed.username.as_str(),
1256 parsed.extensions.clone(),
1257 parsed.token_authenticated(),
1258 ) == (
1259 "n,,",
1260 "token-id",
1261 vec![(TOKEN_AUTH_EXTENSION.to_owned(), "true".to_owned())],
1262 true,
1263 )
1264 );
1265 let StepResult::Continue(s1, server) = server.step(&c1) else {
1266 panic!("server step 1 must continue");
1267 };
1268 let (c2, client) = client.step(&s1).unwrap();
1269 let StepResult::Done(principal, s2) = server.step(&c2) else {
1270 panic!("server step 2 must finish");
1271 };
1272 check!(principal.name == "token-id");
1273 check!(client.verify_server_final(&s2).is_ok());
1274 }
1275
1276 #[test]
1280 fn scram_client_sends_a_repeated_extension_once_and_authenticates() {
1281 let password = b"pw";
1282 let cred = hash_scram_password_with_salt(
1283 password,
1284 SaslMechanism::ScramSha256,
1285 4096,
1286 (0..16).collect::<Vec<u8>>(),
1287 );
1288 let server = ScramServerExchange::new("alice".to_owned(), cred);
1289 let client = ScramClientExchange::new(
1290 "alice".to_owned(),
1291 password.to_vec(),
1292 SaslMechanism::ScramSha256,
1293 )
1294 .with_extensions([("foo", "1"), ("bar", "2"), ("foo", "3")])
1295 .unwrap();
1296
1297 let (c1, client) = client.client_first().unwrap();
1298 let parsed = ScramClientFirst::parse(&c1).unwrap();
1299 check!(
1300 parsed.extensions
1301 == vec![
1302 ("foo".to_owned(), "3".to_owned()),
1303 ("bar".to_owned(), "2".to_owned()),
1304 ]
1305 );
1306 let StepResult::Continue(s1, server) = server.step(&c1) else {
1307 panic!("server step 1 must continue");
1308 };
1309 let (c2, client) = client.step(&s1).unwrap();
1310 let StepResult::Done(principal, s2) = server.step(&c2) else {
1311 panic!("server step 2 must finish");
1312 };
1313 check!(principal.name == "alice");
1314 check!(client.verify_server_final(&s2).is_ok());
1315 }
1316
1317 #[test]
1318 fn scram_client_rejects_extensions_kafka_cannot_parse() {
1319 for (case, key, value) in [
1320 ("empty key", "", "true"),
1321 ("non-alphabetic key", "token_auth", "true"),
1322 ("empty value", "tokenauth", ""),
1323 ("comma in value", "tokenauth", "a,b"),
1324 ("non-ASCII value", "tokenauth", "\u{e9}"),
1325 ] {
1326 let client = ScramClientExchange::new(
1327 "alice".to_owned(),
1328 b"pw".to_vec(),
1329 SaslMechanism::ScramSha256,
1330 );
1331 check!(
1332 client.with_extensions([(key, value)]).map(|_| ())
1333 == Err(crate::AuthError::MalformedMessage),
1334 "case {case}"
1335 );
1336 }
1337 }
1338
1339 #[test]
1342 fn scram_round_trip_escapes_the_username() {
1343 let password = b"pw";
1344 let cred = hash_scram_password_with_salt(
1345 password,
1346 SaslMechanism::ScramSha256,
1347 4096,
1348 (0..16).collect::<Vec<u8>>(),
1349 );
1350 let server = ScramServerExchange::new("a,b=c".to_owned(), cred);
1351 let client = ScramClientExchange::new(
1352 "a,b=c".to_owned(),
1353 password.to_vec(),
1354 SaslMechanism::ScramSha256,
1355 );
1356 let (c1, client) = client.client_first().unwrap();
1357 check!(c1.starts_with(b"n,,n=a=2Cb=3Dc,r="));
1358 let StepResult::Continue(s1, server) = server.step(&c1) else {
1359 panic!("server step 1 must continue");
1360 };
1361 let (c2, _client) = client.step(&s1).unwrap();
1362 let StepResult::Done(principal, _) = server.step(&c2) else {
1363 panic!("server step 2 must finish");
1364 };
1365 check!(principal.name == "a,b=c");
1366 }
1367}