1mod scram;
20
21use std::{
22 collections::BTreeMap,
23 fmt,
24 future::Future,
25 io,
26 path::{Path, PathBuf},
27 sync::{
28 Arc,
29 atomic::{AtomicI64, Ordering},
30 },
31};
32#[cfg(target_os = "wasi")]
33use std::{
34 fs,
35 future::{Ready, ready},
36};
37
38use bytes::{Buf, BufMut, BytesMut};
39use krabka_ids::{ApiKey, ApiVersion};
40#[cfg(test)]
41use krabka_protocol::owned::api_versions_response;
42use krabka_protocol::{
43 Decode, Encode,
44 owned::{
45 api_versions_request::ApiVersionsRequest, api_versions_response::ApiVersionsResponse,
46 sasl_authenticate_request, sasl_authenticate_request::SaslAuthenticateRequest,
47 sasl_authenticate_response::SaslAuthenticateResponse, sasl_handshake_request,
48 sasl_handshake_request::SaslHandshakeRequest,
49 sasl_handshake_response::SaslHandshakeResponse,
50 },
51};
52use krabka_security::SaslMechanism;
53use thiserror::Error;
54#[cfg(not(target_os = "wasi"))]
55use tokio::fs;
56use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
57#[cfg(all(feature = "gssapi", not(target_family = "wasm")))]
58use tokio::task::spawn_blocking;
59
60use crate::{ClientFrameMax, connection::skip_tagged_fields, oauth};
61
62const API_KEY_SASL_HANDSHAKE: i16 = 17;
63const API_KEY_SASL_AUTHENTICATE: i16 = 36;
64const API_KEY_API_VERSIONS: i16 = 18;
65
66const MAX_RESERVED_CORRELATION_ID: i32 = i32::MAX;
68pub(crate) const MIN_RESERVED_CORRELATION_ID: i32 = MAX_RESERVED_CORRELATION_ID - 7;
72
73fn next_correlation_id(corr_id: &mut i32) -> i32 {
76 if *corr_id < MIN_RESERVED_CORRELATION_ID {
77 *corr_id = MIN_RESERVED_CORRELATION_ID;
78 }
79 let current = *corr_id;
80 *corr_id = corr_id.wrapping_add(1);
81 current
82}
83
84#[derive(Clone, Copy)]
85pub(crate) struct SaslPolicy<'a> {
86 client_id: &'a str,
87 frame_max: ClientFrameMax,
88 authenticate_version: Option<i16>,
92 session_lifetime_ms: &'a AtomicI64,
95}
96
97pub(crate) trait SaslChannel: Send {
101 fn request(
104 &mut self,
105 header: (ApiKey, ApiVersion, i32),
106 flexible: bool,
107 body: &[u8],
108 policy: SaslPolicy<'_>,
109 ) -> impl Future<Output = Result<Vec<u8>, OutboundSaslError>> + Send;
110
111 fn token(
113 &mut self,
114 token: &[u8],
115 frame_max: ClientFrameMax,
116 ) -> impl Future<Output = Result<SaslAuthenticateResponse, OutboundSaslError>> + Send;
117}
118
119impl<S> SaslChannel for S
120where
121 S: AsyncRead + AsyncWrite + Unpin + Send + ?Sized,
122{
123 async fn request(
124 &mut self,
125 (api_key, api_version, corr_id): (ApiKey, ApiVersion, i32),
126 flexible: bool,
127 body: &[u8],
128 policy: SaslPolicy<'_>,
129 ) -> Result<Vec<u8>, OutboundSaslError> {
130 round_trip(self, api_key, api_version, corr_id, flexible, body, policy).await
131 }
132
133 async fn token(
134 &mut self,
135 token: &[u8],
136 frame_max: ClientFrameMax,
137 ) -> Result<SaslAuthenticateResponse, OutboundSaslError> {
138 send_raw_token(self, token, frame_max).await
139 }
140}
141
142#[derive(Clone, Copy, Debug, Eq, PartialEq)]
144pub struct SaslSession {
145 pub handshake_version: i16,
147 pub authenticate_version: Option<i16>,
150 pub session_lifetime_ms: Option<i64>,
153}
154
155impl SaslSession {
156 #[must_use]
160 pub fn needs_reauthentication(&self) -> bool {
161 self.session_lifetime_ms.is_some() && self.authenticate_version.is_some_and(|v| v >= 1)
162 }
163}
164
165fn sasl_versions(response: &ApiVersionsResponse) -> (i16, Option<i16>) {
172 let max = |api_key: i16| {
173 response
174 .api_keys
175 .iter()
176 .find(|key| key.api_key == api_key)
177 .map(|key| key.max_version)
178 };
179 let handshake = max(API_KEY_SASL_HANDSHAKE).map_or(0, |version| {
180 version.min(sasl_handshake_request::MAX_VERSION)
181 });
182 let authenticate = max(API_KEY_SASL_AUTHENTICATE)
183 .map(|version| version.min(sasl_authenticate_request::MAX_VERSION));
184 (handshake, authenticate)
185}
186
187#[cfg(all(feature = "gssapi", not(target_family = "wasm")))]
192const GSSAPI_MAX_RECV: krabka_units::ByteSize = krabka_units::kibibytes(64);
193
194#[derive(Clone)]
203pub enum SaslCredentials {
204 Plain { username: String, password: String },
206 Scram {
208 mechanism: SaslMechanism,
209 username: String,
210 password: String,
211 delegation_token: bool,
216 },
217 Gssapi {
223 keytab_path: PathBuf,
224 client_principal: String,
225 service_name: String,
226 kdc_url: String,
227 },
228 OAuthBearer {
230 token: OAuthBearerTokenSource,
232 extensions: BTreeMap<String, String>,
238 },
239}
240
241#[derive(Debug, Clone)]
243pub enum OAuthBearerTokenSource {
244 File(PathBuf),
247 Provider(Arc<dyn oauth::OAuthBearerTokenProvider>),
251}
252
253impl fmt::Debug for SaslCredentials {
254 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
255 const HIDDEN: &str = "[hidden]";
256 match self {
257 Self::Plain { username, .. } => formatter
258 .debug_struct("Plain")
259 .field("username", username)
260 .field("password", &format_args!("{HIDDEN}"))
261 .finish(),
262 Self::Scram {
263 mechanism,
264 username,
265 delegation_token,
266 ..
267 } => formatter
268 .debug_struct("Scram")
269 .field("mechanism", mechanism)
270 .field("username", username)
271 .field("password", &format_args!("{HIDDEN}"))
272 .field("delegation_token", delegation_token)
273 .finish(),
274 Self::Gssapi {
275 keytab_path,
276 client_principal,
277 service_name,
278 kdc_url,
279 } => formatter
280 .debug_struct("Gssapi")
281 .field("keytab_path", keytab_path)
282 .field("client_principal", client_principal)
283 .field("service_name", service_name)
284 .field("kdc_url", kdc_url)
285 .finish(),
286 Self::OAuthBearer { token, extensions } => formatter
287 .debug_struct("OAuthBearer")
288 .field("token", token)
289 .field("extensions", extensions)
290 .finish(),
291 }
292 }
293}
294
295impl SaslCredentials {
296 #[must_use]
298 pub fn mechanism(&self) -> SaslMechanism {
299 match self {
300 Self::Plain { .. } => SaslMechanism::Plain,
301 Self::Scram { mechanism, .. } => *mechanism,
302 Self::Gssapi { .. } => SaslMechanism::Gssapi,
303 Self::OAuthBearer { .. } => SaslMechanism::OAuthBearer,
304 }
305 }
306}
307
308const UNSUPPORTED_SASL_MECHANISM: i16 = 33;
310const ILLEGAL_SASL_STATE: i16 = 34;
312const SASL_AUTHENTICATION_FAILED: i16 = 58;
314
315#[derive(Debug, Error)]
320pub enum OutboundSaslError {
321 #[error("io: {0}")]
325 Io(#[from] io::Error),
326 #[error("codec: {0}")]
329 Codec(String),
330 #[error("SaslAuthenticate error_code={error_code} error_message={error_message:?}")]
334 Server {
335 error_code: i16,
336 error_message: Option<String>,
337 },
338 #[error(transparent)]
340 Authentication(#[from] SaslAuthenticationError),
341}
342
343#[derive(Clone, Debug, Error, PartialEq, Eq)]
347pub enum SaslAuthenticationError {
348 #[error("unsupported SASL mechanism: {0}")]
351 UnsupportedMechanism(String),
352 #[error("illegal SASL state: {0}")]
356 IllegalState(String),
357 #[error("SASL authentication failed: {0}")]
360 Failed(String),
361}
362
363fn authenticate_error(mechanism: &str, response: &SaslAuthenticateResponse) -> OutboundSaslError {
367 let message = format!(
368 "SaslAuthenticate({mechanism}) error_code={} error_message={:?}",
369 response.error_code, response.error_message
370 );
371 match response.error_code {
372 UNSUPPORTED_SASL_MECHANISM => SaslAuthenticationError::UnsupportedMechanism(message).into(),
373 ILLEGAL_SASL_STATE => SaslAuthenticationError::IllegalState(message).into(),
374 SASL_AUTHENTICATION_FAILED => SaslAuthenticationError::Failed(message).into(),
375 error_code => OutboundSaslError::Server {
376 error_code,
377 error_message: response.error_message.clone(),
378 },
379 }
380}
381
382fn mechanism_failure(message: String) -> OutboundSaslError {
386 SaslAuthenticationError::Failed(message).into()
387}
388
389fn unparsable_response(message: String) -> OutboundSaslError {
393 SaslAuthenticationError::IllegalState(message).into()
394}
395
396pub async fn outbound_sasl<S>(
413 stream: &mut S,
414 creds: &SaslCredentials,
415 server_name: &str,
416 client_id: &str,
417 frame_max: ClientFrameMax,
418) -> Result<SaslSession, OutboundSaslError>
419where
420 S: AsyncRead + AsyncWrite + Unpin + Send + ?Sized,
421{
422 let mut corr_id = MIN_RESERVED_CORRELATION_ID;
423 let api_versions = send_api_versions(stream, &mut corr_id, client_id, frame_max).await?;
427 let (handshake_version, authenticate_version) = sasl_versions(&api_versions);
428 authenticate(
430 stream,
431 creds,
432 server_name,
433 SaslSession {
434 handshake_version,
435 authenticate_version,
436 session_lifetime_ms: None,
437 },
438 (&mut corr_id, client_id, frame_max),
439 )
440 .await
441}
442
443pub(crate) async fn authenticate<C>(
450 channel: &mut C,
451 creds: &SaslCredentials,
452 server_name: &str,
453 versions: SaslSession,
454 (corr_id, client_id, frame_max): (&mut i32, &str, ClientFrameMax),
455) -> Result<SaslSession, OutboundSaslError>
456where
457 C: SaslChannel + ?Sized,
458{
459 let session_lifetime_ms = AtomicI64::new(0);
460 let policy = SaslPolicy {
461 client_id,
462 frame_max,
463 authenticate_version: versions.authenticate_version,
464 session_lifetime_ms: &session_lifetime_ms,
465 };
466 send_sasl_handshake(
469 channel,
470 creds.mechanism(),
471 versions.handshake_version,
472 corr_id,
473 policy,
474 )
475 .await?;
476 match creds {
478 SaslCredentials::Plain { username, password } => {
479 send_plain_authenticate(channel, username, password, corr_id, policy).await
480 }
481 SaslCredentials::Scram {
482 mechanism,
483 username,
484 password,
485 delegation_token,
486 } => {
487 run_scram_client(
488 channel,
489 (username, password, *delegation_token),
490 *mechanism,
491 corr_id,
492 policy,
493 )
494 .await
495 }
496 #[cfg(all(feature = "gssapi", not(target_family = "wasm")))]
497 SaslCredentials::Gssapi {
498 keytab_path,
499 client_principal,
500 service_name,
501 kdc_url,
502 } => {
503 run_gssapi_client(
504 channel,
505 keytab_path,
506 client_principal,
507 (service_name, server_name),
508 kdc_url,
509 corr_id,
510 policy,
511 )
512 .await
513 }
514 #[cfg(not(all(feature = "gssapi", not(target_family = "wasm"))))]
517 SaslCredentials::Gssapi { .. } => Err(SaslAuthenticationError::UnsupportedMechanism(
518 format!("GSSAPI to {server_name}: {GSSAPI_UNAVAILABLE}"),
519 )
520 .into()),
521 SaslCredentials::OAuthBearer { token, extensions } => {
522 let token = match token {
525 OAuthBearerTokenSource::File(token_path) => {
526 read_token_file(token_path).await.map_err(|error| {
527 mechanism_failure(format!(
528 "cannot read OAUTHBEARER token {}: {error}",
529 token_path.display()
530 ))
531 })?
532 }
533 OAuthBearerTokenSource::Provider(provider) => provider
534 .token()
535 .await
536 .map_err(|error| {
537 mechanism_failure(format!("cannot get an OAUTHBEARER token: {error}"))
538 })?
539 .into_bytes(),
540 };
541 run_oauthbearer_client(channel, token.trim_ascii(), extensions, corr_id, policy).await
542 }
543 }?;
544 let lifetime = session_lifetime_ms.load(Ordering::Relaxed);
545 Ok(SaslSession {
546 session_lifetime_ms: (lifetime > 0).then_some(lifetime),
547 ..versions
548 })
549}
550
551#[cfg(not(target_os = "wasi"))]
553async fn read_token_file(path: &Path) -> io::Result<Vec<u8>> {
554 fs::read(path).await
555}
556
557#[cfg(target_os = "wasi")]
560fn read_token_file(path: &Path) -> Ready<io::Result<Vec<u8>>> {
561 ready(fs::read(path))
562}
563
564async fn run_oauthbearer_client<S>(
571 stream: &mut S,
572 token: &[u8],
573 extensions: &BTreeMap<String, String>,
574 corr_id: &mut i32,
575 policy: SaslPolicy<'_>,
576) -> Result<(), OutboundSaslError>
577where
578 S: SaslChannel + ?Sized,
579{
580 if token.is_empty() || token.contains(&b'\x01') {
581 return Err(mechanism_failure(
582 "OAUTHBEARER token must be non-empty and contain no RFC 7628 separator".into(),
583 ));
584 }
585
586 let initial = oauthbearer_initial_response(token, extensions)?;
587
588 let response = send_sasl_authenticate(stream, initial, corr_id, policy).await?;
589 if response.error_code != 0 {
590 return Err(authenticate_error("OAUTHBEARER", &response));
591 }
592 if response.auth_bytes.is_empty() {
593 return Ok(());
594 }
595
596 let final_response = send_sasl_authenticate(stream, vec![b'\x01'], corr_id, policy).await?;
597 if final_response.error_code != 0 {
598 return Err(authenticate_error("OAUTHBEARER", &final_response));
599 }
600 Err(mechanism_failure(format!(
601 "SaslAuthenticate(OAUTHBEARER) rejected bearer token: {}",
602 String::from_utf8_lossy(&response.auth_bytes)
603 )))
604}
605
606fn oauthbearer_initial_response(
610 token: &[u8],
611 extensions: &BTreeMap<String, String>,
612) -> Result<Vec<u8>, OutboundSaslError> {
613 for (name, value) in extensions {
614 if name == "auth" {
615 return Err(mechanism_failure("Extension name auth is invalid".into()));
616 }
617 if name.is_empty() || !name.bytes().all(|byte| byte.is_ascii_alphabetic()) {
618 return Err(mechanism_failure(format!(
619 "Extension name {name} is invalid"
620 )));
621 }
622 if value.is_empty()
623 || !value
624 .bytes()
625 .all(|byte| matches!(byte, 0x21..=0x7E | b' ' | b'\t' | b'\r' | b'\n'))
626 {
627 return Err(mechanism_failure(format!(
628 "Extension value ({value}) for extension {name} is invalid"
629 )));
630 }
631 }
632 let mut initial = Vec::with_capacity(token.len() + 20);
633 initial.extend_from_slice(b"n,,\x01auth=Bearer ");
634 initial.extend_from_slice(token);
635 for (name, value) in extensions {
636 initial.push(b'\x01');
637 initial.extend_from_slice(name.as_bytes());
638 initial.push(b'=');
639 initial.extend_from_slice(value.as_bytes());
640 }
641 initial.extend_from_slice(b"\x01\x01");
642 Ok(initial)
643}
644
645async fn send_api_versions<S>(
647 stream: &mut S,
648 corr_id: &mut i32,
649 client_id: &str,
650 frame_max: ClientFrameMax,
651) -> Result<ApiVersionsResponse, OutboundSaslError>
652where
653 S: AsyncRead + AsyncWrite + Unpin + Send + ?Sized,
654{
655 let mut body = BytesMut::new();
656 ApiVersionsRequest::default()
657 .encode(&mut body, 0)
658 .map_err(|e| OutboundSaslError::Codec(format!("ApiVersions encode: {e}")))?;
659 let resp_bytes = round_trip(
660 stream,
661 ApiKey(API_KEY_API_VERSIONS),
662 ApiVersion(0),
663 next_correlation_id(corr_id),
664 false,
665 &body,
666 SaslPolicy {
667 client_id,
668 frame_max,
669 authenticate_version: None,
670 session_lifetime_ms: &AtomicI64::new(0),
671 },
672 )
673 .await?;
674 let mut cur: &[u8] = &resp_bytes;
675 ApiVersionsResponse::decode(&mut cur, 0)
676 .map_err(|e| unparsable_response(format!("ApiVersions decode: {e}")))
677}
678
679async fn send_sasl_handshake<S>(
686 stream: &mut S,
687 mechanism: SaslMechanism,
688 version: i16,
689 corr_id: &mut i32,
690 policy: SaslPolicy<'_>,
691) -> Result<(), OutboundSaslError>
692where
693 S: SaslChannel + ?Sized,
694{
695 let req = SaslHandshakeRequest {
696 mechanism: mechanism.wire_name().to_owned(),
697 ..Default::default()
698 };
699 let mut body = BytesMut::new();
700 req.encode(&mut body, version)
701 .map_err(|e| OutboundSaslError::Codec(format!("SaslHandshake encode: {e}")))?;
702 let resp_bytes = stream
703 .request(
704 (
705 ApiKey(API_KEY_SASL_HANDSHAKE),
706 ApiVersion(version),
707 next_correlation_id(corr_id),
708 ),
709 false,
710 &body,
711 policy,
712 )
713 .await?;
714 let mut cur: &[u8] = &resp_bytes;
715 let resp = SaslHandshakeResponse::decode(&mut cur, version)
716 .map_err(|e| unparsable_response(format!("SaslHandshake decode: {e}")))?;
717 handshake_result(mechanism, &resp)
718}
719
720fn handshake_result(
723 mechanism: SaslMechanism,
724 response: &SaslHandshakeResponse,
725) -> Result<(), OutboundSaslError> {
726 let mechanism = mechanism.wire_name();
727 let enabled = &response.mechanisms;
728 let error = match response.error_code {
729 0 => return Ok(()),
730 UNSUPPORTED_SASL_MECHANISM => SaslAuthenticationError::UnsupportedMechanism(format!(
731 "client SASL mechanism '{mechanism}' not enabled in the server, enabled mechanisms \
732 are {enabled:?}"
733 )),
734 ILLEGAL_SASL_STATE => SaslAuthenticationError::IllegalState(format!(
735 "unexpected handshake request with client mechanism {mechanism}, enabled mechanisms \
736 are {enabled:?}"
737 )),
738 code => SaslAuthenticationError::IllegalState(format!(
739 "unknown error code {code}, client mechanism is {mechanism}, enabled mechanisms are \
740 {enabled:?}"
741 )),
742 };
743 Err(error.into())
744}
745
746async fn send_plain_authenticate<S>(
749 stream: &mut S,
750 user: &str,
751 pass: &str,
752 corr_id: &mut i32,
753 policy: SaslPolicy<'_>,
754) -> Result<(), OutboundSaslError>
755where
756 S: SaslChannel + ?Sized,
757{
758 let mut payload = Vec::with_capacity(2 + user.len() + pass.len());
759 payload.push(0); payload.extend_from_slice(user.as_bytes());
761 payload.push(0);
762 payload.extend_from_slice(pass.as_bytes());
763
764 let resp = send_sasl_authenticate(stream, payload, corr_id, policy).await?;
765 if resp.error_code != 0 {
766 return Err(authenticate_error("PLAIN", &resp));
767 }
768 Ok(())
769}
770
771async fn run_scram_client<S>(
777 stream: &mut S,
778 (user, pass, delegation_token): (&str, &str, bool),
779 mechanism: SaslMechanism,
780 corr_id: &mut i32,
781 policy: SaslPolicy<'_>,
782) -> Result<(), OutboundSaslError>
783where
784 S: SaslChannel + ?Sized,
785{
786 let (client_first, exchange) = scram::client_first(mechanism, user, pass, delegation_token)
788 .map_err(|e| mechanism_failure(format!("scram client_first: {e}")))?;
789 let resp1 = send_sasl_authenticate(stream, client_first, corr_id, policy).await?;
790 if resp1.error_code != 0 {
791 return Err(authenticate_error("SCRAM round 1", &resp1));
792 }
793
794 let (client_final, exchange) = exchange
796 .step(&resp1.auth_bytes)
797 .map_err(|e| mechanism_failure(format!("scram client step: {e}")))?;
798 let resp2 = send_sasl_authenticate(stream, client_final, corr_id, policy).await?;
799 if resp2.error_code != 0 {
800 return Err(authenticate_error("SCRAM round 2", &resp2));
801 }
802 exchange
805 .verify(&resp2.auth_bytes)
806 .map_err(|e| mechanism_failure(format!("server-final verify: {e}")))?;
807 Ok(())
808}
809
810pub(crate) const GSSAPI_CLIENT: bool = cfg!(all(feature = "gssapi", not(target_family = "wasm")));
816
817pub(crate) fn mechanism_supported(mechanism: SaslMechanism) -> bool {
819 mechanism != SaslMechanism::Gssapi || GSSAPI_CLIENT
820}
821
822pub(crate) const GSSAPI_UNAVAILABLE: &str = "this build has no Kerberos client; turn on the \
824 `gssapi` feature of krabka-client-core (or of the admin, consumer or producer crate) on a \
825 native target";
826
827#[cfg(all(feature = "gssapi", not(target_family = "wasm")))]
846async fn run_gssapi_client<S>(
847 stream: &mut S,
848 keytab_path: &Path,
849 client_principal: &str,
850 service_and_server_name: (&str, &str),
851 kdc_url: &str,
852 corr_id: &mut i32,
853 policy: SaslPolicy<'_>,
854) -> Result<(), OutboundSaslError>
855where
856 S: SaslChannel + ?Sized,
857{
858 use krabka_security::gssapi::{
859 client::{ClientStep, GssapiClientExchange},
860 provider::SspiInitiator,
861 };
862
863 let (service_name, server_name) = service_and_server_name;
864 let target_spn = format!("{service_name}/{server_name}");
865 let keytab = keytab_path.to_string_lossy().into_owned();
866 let (client_principal, kdc_url) = (client_principal.to_owned(), kdc_url.to_owned());
867 let mut step = spawn_blocking(move || {
871 let initiator = SspiInitiator::new(&keytab, &client_principal, &target_spn, &kdc_url)
872 .map_err(|e| mechanism_failure(format!("GSSAPI initiator init failed: {e}")))?;
873 let exchange = GssapiClientExchange::new(Box::new(initiator), GSSAPI_MAX_RECV, None);
874 exchange
876 .step(None)
877 .map_err(|e| mechanism_failure(format!("GSSAPI initiate failed: {e}")))
878 })
879 .await
880 .map_err(|e| mechanism_failure(format!("GSSAPI initiate task failed: {e}")))??;
881 loop {
882 match step {
883 ClientStep::Token(token, next) => {
884 let resp = send_sasl_authenticate(stream, token, corr_id, policy).await?;
885 if resp.error_code != 0 {
886 return Err(authenticate_error("GSSAPI", &resp));
887 }
888 step = next
889 .step(Some(&resp.auth_bytes))
890 .map_err(|e| mechanism_failure(format!("GSSAPI step failed: {e}")))?;
891 }
892 ClientStep::Final(token) => {
893 let resp = send_sasl_authenticate(stream, token, corr_id, policy).await?;
894 if resp.error_code != 0 {
895 return Err(authenticate_error("GSSAPI", &resp));
896 }
897 return Ok(());
898 }
899 }
900 }
901}
902
903async fn send_sasl_authenticate<S>(
911 stream: &mut S,
912 auth_bytes: Vec<u8>,
913 corr_id: &mut i32,
914 policy: SaslPolicy<'_>,
915) -> Result<SaslAuthenticateResponse, OutboundSaslError>
916where
917 S: SaslChannel + ?Sized,
918{
919 let Some(version) = policy.authenticate_version else {
920 return stream.token(&auth_bytes, policy.frame_max).await;
921 };
922 let req = SaslAuthenticateRequest {
923 auth_bytes: bytes::Bytes::from(auth_bytes),
924 ..Default::default()
925 };
926 let mut body = BytesMut::new();
927 req.encode(&mut body, version)
928 .map_err(|e| OutboundSaslError::Codec(format!("SaslAuthenticate encode: {e}")))?;
929 let resp_bytes = stream
930 .request(
931 (
932 ApiKey(API_KEY_SASL_AUTHENTICATE),
933 ApiVersion(version),
934 next_correlation_id(corr_id),
935 ),
936 version >= sasl_authenticate_request::FLEXIBLE_MIN,
937 &body,
938 policy,
939 )
940 .await?;
941 let mut cur: &[u8] = &resp_bytes;
942 let resp = SaslAuthenticateResponse::decode(&mut cur, version)
943 .map_err(|e| unparsable_response(format!("SaslAuthenticate decode: {e}")))?;
944 if resp.error_code == 0 && resp.session_lifetime_ms > 0 {
947 policy
948 .session_lifetime_ms
949 .store(resp.session_lifetime_ms, Ordering::Relaxed);
950 }
951 Ok(resp)
952}
953
954async fn send_raw_token<S>(
956 stream: &mut S,
957 token: &[u8],
958 frame_max: ClientFrameMax,
959) -> Result<SaslAuthenticateResponse, OutboundSaslError>
960where
961 S: AsyncRead + AsyncWrite + Unpin + Send + ?Sized,
962{
963 if token.len() > frame_max.bytes() {
964 return Err(OutboundSaslError::Codec(format!(
965 "SASL token encoded {} bytes, maximum {}",
966 token.len(),
967 frame_max.bytes()
968 )));
969 }
970 stream
971 .write_u32(
972 u32::try_from(token.len())
973 .map_err(|_| OutboundSaslError::Codec("token size exceeds u32".into()))?,
974 )
975 .await?;
976 stream.write_all(token).await?;
977 stream.flush().await?;
978 let len = usize::try_from(stream.read_u32().await?)
979 .map_err(|_| OutboundSaslError::Codec("SASL token length does not fit usize".into()))?;
980 if len > frame_max.bytes() {
981 return Err(OutboundSaslError::Codec(format!(
982 "SASL token announced {len} bytes, maximum {}",
983 frame_max.bytes()
984 )));
985 }
986 let mut answer = vec![0_u8; len];
987 stream.read_exact(&mut answer).await?;
988 Ok(SaslAuthenticateResponse {
989 auth_bytes: bytes::Bytes::from(answer),
990 ..Default::default()
991 })
992}
993
994async fn round_trip<S>(
1007 stream: &mut S,
1008 api_key: ApiKey,
1009 api_version: ApiVersion,
1010 corr_id: i32,
1011 flexible: bool,
1012 body: &[u8],
1013 policy: SaslPolicy<'_>,
1014) -> Result<Vec<u8>, OutboundSaslError>
1015where
1016 S: AsyncRead + AsyncWrite + Unpin + Send + ?Sized,
1017{
1018 let SaslPolicy {
1019 client_id,
1020 frame_max,
1021 ..
1022 } = policy;
1023 let mut frame = BytesMut::with_capacity(16 + body.len());
1024 frame.put_i16(api_key.0);
1026 frame.put_i16(api_version.0);
1027 frame.put_i32(corr_id);
1028 frame.put_i16(
1029 i16::try_from(client_id.len())
1030 .map_err(|_| OutboundSaslError::Codec("client_id too long".into()))?,
1031 );
1032 frame.put_slice(client_id.as_bytes());
1033 if flexible {
1034 frame.put_u8(0); }
1036 frame.put_slice(body);
1037
1038 if frame.len() > frame_max.bytes() {
1039 return Err(OutboundSaslError::Codec(format!(
1040 "SASL request encoded {} bytes, maximum {}",
1041 frame.len(),
1042 frame_max.bytes()
1043 )));
1044 }
1045
1046 stream
1047 .write_u32(
1048 u32::try_from(frame.len())
1049 .map_err(|_| OutboundSaslError::Codec("frame size exceeds u32".into()))?,
1050 )
1051 .await?;
1052 stream.write_all(&frame).await?;
1053 stream.flush().await?;
1054
1055 let resp_len = usize::try_from(stream.read_u32().await?)
1057 .map_err(|_| OutboundSaslError::Codec("SASL response length does not fit usize".into()))?;
1058 if resp_len > frame_max.bytes() {
1059 return Err(OutboundSaslError::Codec(format!(
1060 "SASL response announced {resp_len} bytes, maximum {}",
1061 frame_max.bytes()
1062 )));
1063 }
1064 let mut resp = vec![0u8; resp_len];
1065 stream.read_exact(&mut resp).await?;
1066
1067 let mut cur = &resp[..];
1071 if cur.len() < 4 {
1072 return Err(unparsable_response("response missing corr_id".into()));
1073 }
1074 let _resp_corr_id = cur.get_i32();
1075 let uses_v1_header = flexible && api_key != 18;
1076 if uses_v1_header {
1077 if cur.is_empty() {
1078 return Err(unparsable_response(
1079 "flexible response missing tagged-fields byte".into(),
1080 ));
1081 }
1082 cur = skip_tagged_fields(cur).map_err(|error| unparsable_response(error.to_string()))?;
1083 }
1084 Ok(cur.to_vec())
1085}
1086
1087#[cfg(test)]
1088mod tests {
1089 use std::{fs, future::ready, iter::repeat_with};
1090
1091 use assert2::check;
1092 use krabka_protocol::owned::{
1093 sasl_authenticate_response::SaslAuthenticateResponse,
1094 sasl_handshake_response::SaslHandshakeResponse,
1095 };
1096 use krabka_security::{ScramServerExchange, StepResult, hash_scram_password};
1097 use tokio::{
1098 io::{AsyncReadExt, AsyncWriteExt, duplex},
1099 time::{Duration, timeout},
1100 };
1101
1102 use super::*;
1103
1104 #[test]
1105 fn debug_hides_every_password() {
1106 let cases = [
1107 (
1108 SaslCredentials::Plain {
1109 username: "alice".into(),
1110 password: "plain-secret".into(),
1111 },
1112 r#"Plain { username: "alice", password: [hidden] }"#,
1113 ),
1114 (
1115 SaslCredentials::Scram {
1116 mechanism: SaslMechanism::ScramSha512,
1117 username: "token-id".into(),
1118 password: "token-hmac".into(),
1119 delegation_token: true,
1120 },
1121 r#"Scram { mechanism: ScramSha512, username: "token-id", password: [hidden], delegation_token: true }"#,
1122 ),
1123 (
1124 SaslCredentials::Gssapi {
1125 keytab_path: PathBuf::from("/etc/krb5.keytab"),
1126 client_principal: "kafka/[email protected]".into(),
1127 service_name: "kafka".into(),
1128 kdc_url: "kdc.example.com:88".into(),
1129 },
1130 r#"Gssapi { keytab_path: "/etc/krb5.keytab", client_principal: "kafka/[email protected]", service_name: "kafka", kdc_url: "kdc.example.com:88" }"#,
1131 ),
1132 (
1133 SaslCredentials::OAuthBearer {
1134 token: OAuthBearerTokenSource::File(PathBuf::from("/run/token")),
1135 extensions: maplit::btreemap! {"logicalCluster".into() => "lkc-1".into()},
1136 },
1137 r#"OAuthBearer { token: File("/run/token"), extensions: {"logicalCluster": "lkc-1"} }"#,
1138 ),
1139 ];
1140 for (credentials, expected) in cases {
1141 check!(format!("{credentials:?}") == expected);
1142 }
1143 }
1144
1145 const TEST_CLIENT_ID: &str = "configured-sasl-client";
1146
1147 async fn reply_frame<S>(stream: &mut S, body: &[u8], flex_header: bool) -> Vec<u8>
1152 where
1153 S: AsyncRead + AsyncWrite + Unpin,
1154 {
1155 let req_len = stream.read_u32().await.unwrap();
1156 let mut req = vec![0u8; req_len as usize];
1157 stream.read_exact(&mut req).await.unwrap();
1158 let corr_id = i32::from_be_bytes([req[4], req[5], req[6], req[7]]);
1160 let mut frame = BytesMut::new();
1161 frame.put_i32(corr_id);
1162 if flex_header {
1163 frame.put_u8(0); }
1165 frame.put_slice(body);
1166 stream
1167 .write_u32(u32::try_from(frame.len()).unwrap())
1168 .await
1169 .unwrap();
1170 stream.write_all(&frame).await.unwrap();
1171 stream.flush().await.unwrap();
1172 req
1173 }
1174
1175 async fn answer_api_versions<S>(stream: &mut S)
1178 where
1179 S: AsyncRead + AsyncWrite + Unpin,
1180 {
1181 use api_versions_response::ApiVersion as Advertised;
1182
1183 let mut body = BytesMut::new();
1184 ApiVersionsResponse {
1185 api_keys: vec![
1186 Advertised {
1187 api_key: API_KEY_SASL_HANDSHAKE,
1188 min_version: 0,
1189 max_version: 1,
1190 ..Default::default()
1191 },
1192 Advertised {
1193 api_key: API_KEY_SASL_AUTHENTICATE,
1194 min_version: 0,
1195 max_version: 2,
1196 ..Default::default()
1197 },
1198 ],
1199 ..Default::default()
1200 }
1201 .encode(&mut body, 0)
1202 .unwrap();
1203 let request = reply_frame(stream, &body, false).await;
1204 assert_request_header(
1205 &request,
1206 ApiKey(API_KEY_API_VERSIONS),
1207 ApiVersion(0),
1208 MIN_RESERVED_CORRELATION_ID,
1209 false,
1210 );
1211 }
1212
1213 macro_rules! decode_sasl_authenticate_frame {
1214 ($req:expr, $corr_id:expr) => {{
1215 assert_request_header(
1216 &$req,
1217 ApiKey(API_KEY_SASL_AUTHENTICATE),
1218 ApiVersion(2),
1219 $corr_id,
1220 true,
1221 );
1222 let mut body = request_body(&$req, true);
1223 let decoded = SaslAuthenticateRequest::decode(&mut body, 2).unwrap();
1224 assert!(body.is_empty());
1225 decoded
1226 }};
1227 }
1228
1229 #[derive(Debug, PartialEq, Eq)]
1231 enum Frame {
1232 Request(i16, i16, i32),
1234 Token(Vec<u8>),
1236 }
1237
1238 #[tokio::test]
1244 async fn plain_exchange_uses_the_sasl_versions_that_api_versions_lists() {
1245 use api_versions_response::ApiVersion as Advertised;
1246
1247 const C: i32 = MIN_RESERVED_CORRELATION_ID;
1248 for (name, listed, expected) in [
1249 (
1250 "current broker",
1251 (Some(1), Some(2)),
1252 vec![
1253 Frame::Request(API_KEY_API_VERSIONS, 0, C),
1254 Frame::Request(API_KEY_SASL_HANDSHAKE, 1, C + 1),
1255 Frame::Request(API_KEY_SASL_AUTHENTICATE, 2, C + 2),
1256 ],
1257 ),
1258 (
1259 "SaslAuthenticate v1",
1260 (Some(1), Some(1)),
1261 vec![
1262 Frame::Request(API_KEY_API_VERSIONS, 0, C),
1263 Frame::Request(API_KEY_SASL_HANDSHAKE, 1, C + 1),
1264 Frame::Request(API_KEY_SASL_AUTHENTICATE, 1, C + 2),
1265 ],
1266 ),
1267 (
1268 "no SaslAuthenticate",
1269 (Some(0), None),
1270 vec![
1271 Frame::Request(API_KEY_API_VERSIONS, 0, C),
1272 Frame::Request(API_KEY_SASL_HANDSHAKE, 0, C + 1),
1273 Frame::Token(b"\0u\0p".to_vec()),
1274 ],
1275 ),
1276 ] {
1277 let (mut client, mut server) = duplex(8192);
1278 let server_task = tokio::spawn(async move {
1279 let mut frames = Vec::new();
1280 for _ in 0..3 {
1281 let len = server.read_u32().await.unwrap();
1282 let mut request = vec![0_u8; len as usize];
1283 server.read_exact(&mut request).await.unwrap();
1284 if frames.len() == 2 && listed.1.is_none() {
1285 frames.push(Frame::Token(request));
1286 server.write_u32(0).await.unwrap();
1287 continue;
1288 }
1289 let api_key = i16::from_be_bytes([request[0], request[1]]);
1290 let version = i16::from_be_bytes([request[2], request[3]]);
1291 let corr_id =
1292 i32::from_be_bytes([request[4], request[5], request[6], request[7]]);
1293 frames.push(Frame::Request(api_key, version, corr_id));
1294 let mut body = BytesMut::new();
1295 body.put_i32(corr_id);
1296 match api_key {
1297 API_KEY_API_VERSIONS => ApiVersionsResponse {
1298 api_keys: [
1299 (API_KEY_SASL_HANDSHAKE, listed.0),
1300 (API_KEY_SASL_AUTHENTICATE, listed.1),
1301 ]
1302 .into_iter()
1303 .filter_map(|(api_key, max)| {
1304 max.map(|max_version| Advertised {
1305 api_key,
1306 min_version: 0,
1307 max_version,
1308 ..Default::default()
1309 })
1310 })
1311 .collect(),
1312 ..Default::default()
1313 }
1314 .encode(&mut body, 0)
1315 .unwrap(),
1316 API_KEY_SASL_HANDSHAKE => SaslHandshakeResponse::default()
1317 .encode(&mut body, version)
1318 .unwrap(),
1319 _ => {
1320 if version >= 2 {
1321 body.put_u8(0);
1322 }
1323 SaslAuthenticateResponse::default()
1324 .encode(&mut body, version)
1325 .unwrap();
1326 }
1327 }
1328 server
1329 .write_u32(u32::try_from(body.len()).unwrap())
1330 .await
1331 .unwrap();
1332 server.write_all(&body).await.unwrap();
1333 }
1334 frames
1335 });
1336 let result = outbound_sasl(
1337 &mut client,
1338 &SaslCredentials::Plain {
1339 username: "u".into(),
1340 password: "p".into(),
1341 },
1342 "localhost",
1343 TEST_CLIENT_ID,
1344 ClientFrameMax::default(),
1345 )
1346 .await;
1347 let frames = timeout(Duration::from_secs(1), server_task)
1348 .await
1349 .expect("server saw three frames")
1350 .unwrap();
1351 check!(result.is_ok(), "{name}: {result:?}");
1352 check!(frames == expected, "{name}");
1353 }
1354 }
1355
1356 #[test]
1359 fn oauthbearer_initial_responses_carry_valid_extensions() {
1360 let extensions = |pairs: &[(&str, &str)]| {
1361 pairs
1362 .iter()
1363 .map(|(name, value)| ((*name).to_owned(), (*value).to_owned()))
1364 .collect::<BTreeMap<_, _>>()
1365 };
1366 for (name, pairs, expected) in [
1367 ("no extensions", vec![], Ok(b"n,,\x01auth=Bearer t\x01\x01".to_vec())),
1368 (
1369 "two extensions",
1370 vec![("logicalCluster", "lkc-1"), ("identityPoolId", "pool-2")],
1371 Ok(b"n,,\x01auth=Bearer t\x01identityPoolId=pool-2\x01logicalCluster=lkc-1\x01\x01"
1372 .to_vec()),
1373 ),
1374 (
1375 "reserved name",
1376 vec![("auth", "x")],
1377 Err("Extension name auth is invalid".to_owned()),
1378 ),
1379 (
1380 "name with a digit",
1381 vec![("cluster1", "x")],
1382 Err("Extension name cluster1 is invalid".to_owned()),
1383 ),
1384 (
1385 "value with a control character",
1386 vec![("cluster", "a\x01b")],
1387 Err("Extension value (a\x01b) for extension cluster is invalid".to_owned()),
1388 ),
1389 ] {
1390 let result = oauthbearer_initial_response(b"t", &extensions(&pairs)).map_err(|error| {
1391 match error {
1392 OutboundSaslError::Authentication(SaslAuthenticationError::Failed(message)) => {
1393 message
1394 }
1395 other => other.to_string(),
1396 }
1397 });
1398 check!(result == expected, "{name}");
1399 }
1400 }
1401
1402 struct RecordingChannel {
1405 tokens: Vec<Vec<u8>>,
1406 server: Option<krabka_security::ScramServerExchange>,
1407 }
1408
1409 impl RecordingChannel {
1410 fn answer(&mut self, token: &[u8]) -> Vec<u8> {
1413 self.tokens.push(token.to_vec());
1414 match self.server.take() {
1415 Some(server) => match server.step(token) {
1416 StepResult::Continue(bytes, next) => {
1417 self.server = Some(next);
1418 bytes
1419 }
1420 StepResult::Done(_, bytes) => bytes,
1421 StepResult::Failed(error) => panic!("SCRAM server failed: {error:?}"),
1422 StepResult::Disconnect => panic!("SCRAM server disconnected"),
1423 },
1424 None => Vec::new(),
1425 }
1426 }
1427 }
1428
1429 impl SaslChannel for RecordingChannel {
1430 fn request(
1431 &mut self,
1432 (api_key, version, _corr_id): (ApiKey, ApiVersion, i32),
1433 _flexible: bool,
1434 body: &[u8],
1435 _policy: SaslPolicy<'_>,
1436 ) -> impl Future<Output = Result<Vec<u8>, OutboundSaslError>> + Send {
1437 let mut out = BytesMut::new();
1438 if api_key == ApiKey(API_KEY_SASL_HANDSHAKE) {
1439 SaslHandshakeResponse::default()
1440 .encode(&mut out, version.0)
1441 .unwrap();
1442 return ready(Ok(out.to_vec()));
1443 }
1444 let mut cursor = body;
1445 let request = SaslAuthenticateRequest::decode(&mut cursor, version.0).unwrap();
1446 SaslAuthenticateResponse {
1447 auth_bytes: self.answer(&request.auth_bytes).into(),
1448 ..Default::default()
1449 }
1450 .encode(&mut out, version.0)
1451 .unwrap();
1452 ready(Ok(out.to_vec()))
1453 }
1454
1455 fn token(
1456 &mut self,
1457 token: &[u8],
1458 _frame_max: ClientFrameMax,
1459 ) -> impl Future<Output = Result<SaslAuthenticateResponse, OutboundSaslError>> + Send
1460 {
1461 ready(Ok(SaslAuthenticateResponse {
1462 auth_bytes: self.answer(token).into(),
1463 ..Default::default()
1464 }))
1465 }
1466 }
1467
1468 #[derive(Debug)]
1469 struct FixedToken;
1470
1471 impl oauth::OAuthBearerTokenProvider for FixedToken {
1472 fn token(&self) -> oauth::TokenFuture<'_> {
1473 Box::pin(async { Ok("provided.token".to_owned()) })
1474 }
1475 }
1476
1477 #[tokio::test]
1480 async fn credentials_send_provider_tokens_extensions_and_delegation_token_login() {
1481 let sessions = [
1482 (
1483 "SaslAuthenticate v1",
1484 SaslSession {
1485 handshake_version: 1,
1486 authenticate_version: Some(1),
1487 session_lifetime_ms: None,
1488 },
1489 ),
1490 (
1491 "headerless",
1492 SaslSession {
1493 handshake_version: 0,
1494 authenticate_version: None,
1495 session_lifetime_ms: None,
1496 },
1497 ),
1498 ];
1499 let scram_server = || {
1500 Some(ScramServerExchange::new(
1501 "token-id".into(),
1502 hash_scram_password(b"hmac", SaslMechanism::ScramSha256, 4096),
1503 ))
1504 };
1505 for (session_name, session) in sessions {
1506 for (name, credentials, server, check_first) in [
1507 (
1508 "OAUTHBEARER provider with an extension",
1509 SaslCredentials::OAuthBearer {
1510 token: OAuthBearerTokenSource::Provider(Arc::new(FixedToken)),
1511 extensions: maplit::btreemap! {"logicalCluster".into() => "lkc-1".into()},
1512 },
1513 None,
1514 Box::new(|first: &[u8]| {
1515 first
1516 == b"n,,\x01auth=Bearer provided.token\x01logicalCluster=lkc-1\x01\x01"
1517 }) as Box<dyn Fn(&[u8]) -> bool>,
1518 ),
1519 (
1520 "SCRAM delegation token",
1521 SaslCredentials::Scram {
1522 mechanism: SaslMechanism::ScramSha256,
1523 username: "token-id".into(),
1524 password: "hmac".into(),
1525 delegation_token: true,
1526 },
1527 scram_server(),
1528 Box::new(|first: &[u8]| {
1529 let first = String::from_utf8_lossy(first);
1530 first.starts_with("n,,n=token-id,r=") && first.ends_with(",tokenauth=true")
1531 }),
1532 ),
1533 ] {
1534 let mut channel = RecordingChannel {
1535 tokens: Vec::new(),
1536 server,
1537 };
1538 let mut corr_id = 0;
1539 let result = authenticate(
1540 &mut channel,
1541 &credentials,
1542 "localhost",
1543 session,
1544 (&mut corr_id, TEST_CLIENT_ID, ClientFrameMax::default()),
1545 )
1546 .await;
1547 check!(result.is_ok(), "{session_name}, {name}: {result:?}");
1548 check!(
1549 check_first(&channel.tokens[0]),
1550 "{session_name}, {name}: {:?}",
1551 String::from_utf8_lossy(&channel.tokens[0])
1552 );
1553 }
1554 }
1555 }
1556
1557 #[test]
1558 fn sasl_correlation_ids_stay_in_the_reserved_range() {
1559 let mut corr_id = i32::MAX - 1;
1560 let ids = repeat_with(|| next_correlation_id(&mut corr_id))
1561 .take(4)
1562 .collect::<Vec<_>>();
1563 check!(
1564 ids == vec![
1565 i32::MAX - 1,
1566 i32::MAX,
1567 MIN_RESERVED_CORRELATION_ID,
1568 MIN_RESERVED_CORRELATION_ID + 1
1569 ]
1570 );
1571 }
1572
1573 #[cfg(not(all(feature = "gssapi", not(target_family = "wasm"))))]
1576 #[tokio::test]
1577 async fn gssapi_without_the_kerberos_client_is_an_unsupported_mechanism() {
1578 let (mut client, mut server) = duplex(8192);
1579 let server_task = tokio::spawn(async move {
1580 answer_api_versions(&mut server).await;
1581 let mut hs = BytesMut::new();
1582 SaslHandshakeResponse {
1583 error_code: 0,
1584 mechanisms: vec!["GSSAPI".into()],
1585 ..Default::default()
1586 }
1587 .encode(&mut hs, 1)
1588 .unwrap();
1589 let hs_req = reply_frame(&mut server, &hs, false).await;
1590 let mut hs_body = request_body(&hs_req, false);
1591 SaslHandshakeRequest::decode(&mut hs_body, 1)
1592 .unwrap()
1593 .mechanism
1594 });
1595 let creds = SaslCredentials::Gssapi {
1596 keytab_path: PathBuf::from("/etc/krb5.keytab"),
1597 client_principal: "[email protected]".into(),
1598 service_name: "kafka".into(),
1599 kdc_url: "kdc.example.com:88".into(),
1600 };
1601 let result = outbound_sasl(
1602 &mut client,
1603 &creds,
1604 "broker-1.example.com",
1605 TEST_CLIENT_ID,
1606 ClientFrameMax::default(),
1607 )
1608 .await;
1609 drop(client);
1610 let mechanism = timeout(Duration::from_secs(1), server_task)
1611 .await
1612 .expect("server observed the handshake")
1613 .unwrap();
1614 check!(
1615 (verdict(result.map(|_| ())), mechanism)
1616 == (
1617 Verdict::Rejected(SaslAuthenticationError::UnsupportedMechanism(format!(
1618 "GSSAPI to broker-1.example.com: {GSSAPI_UNAVAILABLE}"
1619 ))),
1620 "GSSAPI".to_owned(),
1621 )
1622 );
1623 }
1624
1625 #[tokio::test]
1626 async fn outbound_plain_completes() {
1627 let (mut client, mut server) = duplex(8192);
1628 let server_task = tokio::spawn(async move {
1629 answer_api_versions(&mut server).await;
1630 let mut hs = BytesMut::new();
1632 SaslHandshakeResponse {
1633 error_code: 0,
1634 ..Default::default()
1635 }
1636 .encode(&mut hs, 1)
1637 .unwrap();
1638 let hs_req = reply_frame(&mut server, &hs, false).await;
1639 assert_request_header(
1640 &hs_req,
1641 ApiKey(API_KEY_SASL_HANDSHAKE),
1642 ApiVersion(1),
1643 MIN_RESERVED_CORRELATION_ID + 1,
1644 false,
1645 );
1646 let mut hs_body = request_body(&hs_req, false);
1647 let hs_decoded = SaslHandshakeRequest::decode(&mut hs_body, 1).unwrap();
1648 assert_eq!(hs_decoded.mechanism, "PLAIN");
1649 assert!(hs_body.is_empty());
1650
1651 let mut au = BytesMut::new();
1653 SaslAuthenticateResponse {
1654 error_code: 0,
1655 ..Default::default()
1656 }
1657 .encode(&mut au, 2)
1658 .unwrap();
1659 let au_req = reply_frame(&mut server, &au, true).await;
1660 let au_decoded =
1661 decode_sasl_authenticate_frame!(au_req, MIN_RESERVED_CORRELATION_ID + 2);
1662 assert_eq!(au_decoded.auth_bytes.as_ref(), b"\0u\0p");
1663 });
1664 let creds = SaslCredentials::Plain {
1665 username: "u".into(),
1666 password: "p".into(),
1667 };
1668 outbound_sasl(
1669 &mut client,
1670 &creds,
1671 "localhost",
1672 TEST_CLIENT_ID,
1673 ClientFrameMax::default(),
1674 )
1675 .await
1676 .expect("PLAIN outbound handshake completes");
1677 timeout(Duration::from_secs(1), server_task)
1678 .await
1679 .expect("server observed both SASL client frames")
1680 .unwrap();
1681 }
1682
1683 #[tokio::test]
1684 async fn outbound_oauthbearer_sends_rfc7628_initial_response_and_rereads_token_file() {
1685 let dir = tempfile::tempdir().unwrap();
1686 let token_path = dir.path().join("token");
1687 for token in ["header.payload.", "rotated.token."] {
1688 fs::write(&token_path, format!("{token}\n")).unwrap();
1689 let expected = format!("n,,\x01auth=Bearer {token}\x01\x01").into_bytes();
1690 let (mut client, mut server) = duplex(8192);
1691 let server_task = tokio::spawn(async move {
1692 answer_api_versions(&mut server).await;
1693 let mut handshake = BytesMut::new();
1694 SaslHandshakeResponse {
1695 error_code: 0,
1696 ..Default::default()
1697 }
1698 .encode(&mut handshake, 1)
1699 .unwrap();
1700 let request = reply_frame(&mut server, &handshake, false).await;
1701 let mut body = request_body(&request, false);
1702 let decoded = SaslHandshakeRequest::decode(&mut body, 1).unwrap();
1703 check!(decoded.mechanism == "OAUTHBEARER");
1704
1705 let mut authenticate = BytesMut::new();
1706 SaslAuthenticateResponse {
1707 error_code: 0,
1708 auth_bytes: bytes::Bytes::new(),
1709 session_lifetime_ms: 60_000,
1710 ..Default::default()
1711 }
1712 .encode(&mut authenticate, 2)
1713 .unwrap();
1714 let request = reply_frame(&mut server, &authenticate, true).await;
1715 let decoded =
1716 decode_sasl_authenticate_frame!(request, MIN_RESERVED_CORRELATION_ID + 2);
1717 check!(decoded.auth_bytes.as_ref() == expected);
1718 });
1719
1720 let credentials = SaslCredentials::OAuthBearer {
1721 token: OAuthBearerTokenSource::File(token_path.clone()),
1722 extensions: BTreeMap::new(),
1723 };
1724 outbound_sasl(
1725 &mut client,
1726 &credentials,
1727 "localhost",
1728 TEST_CLIENT_ID,
1729 ClientFrameMax::default(),
1730 )
1731 .await
1732 .expect("OAUTHBEARER outbound handshake completes");
1733 timeout(Duration::from_secs(1), server_task)
1734 .await
1735 .expect("server observed OAUTHBEARER frames")
1736 .unwrap();
1737 }
1738 }
1739
1740 #[tokio::test]
1741 async fn outbound_oauthbearer_completes_rfc7628_rejection_exchange() {
1742 let dir = tempfile::tempdir().unwrap();
1743 let token_path = dir.path().join("token");
1744 fs::write(&token_path, "invalid").unwrap();
1745 let (mut client, mut server) = duplex(8192);
1746 let server_task = tokio::spawn(async move {
1747 answer_api_versions(&mut server).await;
1748 let mut handshake = BytesMut::new();
1749 SaslHandshakeResponse {
1750 error_code: 0,
1751 ..Default::default()
1752 }
1753 .encode(&mut handshake, 1)
1754 .unwrap();
1755 let _ = reply_frame(&mut server, &handshake, false).await;
1756
1757 let mut challenge = BytesMut::new();
1758 SaslAuthenticateResponse {
1759 error_code: 0,
1760 auth_bytes: bytes::Bytes::from_static(br#"{"status":"invalid_token"}"#),
1761 ..Default::default()
1762 }
1763 .encode(&mut challenge, 2)
1764 .unwrap();
1765 let first = reply_frame(&mut server, &challenge, true).await;
1766 let _ = decode_sasl_authenticate_frame!(first, MIN_RESERVED_CORRELATION_ID + 2);
1767
1768 let mut rejected = BytesMut::new();
1769 SaslAuthenticateResponse {
1770 error_code: 58,
1771 error_message: Some("oauthbearer token rejected".into()),
1772 ..Default::default()
1773 }
1774 .encode(&mut rejected, 2)
1775 .unwrap();
1776 let final_request = reply_frame(&mut server, &rejected, true).await;
1777 let decoded =
1778 decode_sasl_authenticate_frame!(final_request, MIN_RESERVED_CORRELATION_ID + 3);
1779 check!(decoded.auth_bytes.as_ref() == b"\x01");
1780 });
1781
1782 let credentials = SaslCredentials::OAuthBearer {
1783 token: OAuthBearerTokenSource::File(token_path),
1784 extensions: BTreeMap::new(),
1785 };
1786 let error = outbound_sasl(
1787 &mut client,
1788 &credentials,
1789 "localhost",
1790 TEST_CLIENT_ID,
1791 ClientFrameMax::default(),
1792 )
1793 .await
1794 .expect_err("invalid bearer token is rejected");
1795 check!(matches!(
1796 error,
1797 OutboundSaslError::Authentication(SaslAuthenticationError::Failed(message))
1798 if message
1799 == "SaslAuthenticate(OAUTHBEARER) error_code=58 \
1800 error_message=Some(\"oauthbearer token rejected\")"
1801 ));
1802 timeout(Duration::from_secs(1), server_task)
1803 .await
1804 .expect("server observed RFC 7628 final message")
1805 .unwrap();
1806 }
1807
1808 #[tokio::test]
1809 async fn send_sasl_authenticate_increments_correlation_id_and_sends_auth_bytes() {
1810 let (mut client, mut server) = duplex(8192);
1811 let server_task = tokio::spawn(async move {
1812 for (expected_corr, expected_payload) in [
1813 (MIN_RESERVED_CORRELATION_ID, b"first".as_ref()),
1814 (MIN_RESERVED_CORRELATION_ID + 1, b"second".as_ref()),
1815 ] {
1816 let mut au = BytesMut::new();
1817 SaslAuthenticateResponse {
1818 error_code: 0,
1819 ..Default::default()
1820 }
1821 .encode(&mut au, 2)
1822 .unwrap();
1823 let req = reply_frame(&mut server, &au, true).await;
1824 let decoded = decode_sasl_authenticate_frame!(req, expected_corr);
1825 assert_eq!(decoded.auth_bytes.as_ref(), expected_payload);
1826 }
1827 });
1828
1829 let mut corr_id = 7;
1832 send_sasl_authenticate(
1833 &mut client,
1834 b"first".to_vec(),
1835 &mut corr_id,
1836 SaslPolicy {
1837 client_id: TEST_CLIENT_ID,
1838 frame_max: ClientFrameMax::default(),
1839 authenticate_version: Some(2),
1840 session_lifetime_ms: &AtomicI64::new(0),
1841 },
1842 )
1843 .await
1844 .unwrap();
1845 assert_eq!(corr_id, MIN_RESERVED_CORRELATION_ID + 1);
1846 send_sasl_authenticate(
1847 &mut client,
1848 b"second".to_vec(),
1849 &mut corr_id,
1850 SaslPolicy {
1851 client_id: TEST_CLIENT_ID,
1852 frame_max: ClientFrameMax::default(),
1853 authenticate_version: Some(2),
1854 session_lifetime_ms: &AtomicI64::new(0),
1855 },
1856 )
1857 .await
1858 .unwrap();
1859 assert_eq!(corr_id, MIN_RESERVED_CORRELATION_ID + 2);
1860 timeout(Duration::from_secs(1), server_task)
1861 .await
1862 .expect("server observed both authenticate frames")
1863 .unwrap();
1864 }
1865
1866 #[tokio::test]
1867 async fn outbound_scram_rejects_broker_error_on_first_round() {
1868 let (mut client, mut server) = duplex(8192);
1869 let server_task = tokio::spawn(async move {
1870 answer_api_versions(&mut server).await;
1871 let mut hs = BytesMut::new();
1872 SaslHandshakeResponse {
1873 error_code: 0,
1874 ..Default::default()
1875 }
1876 .encode(&mut hs, 1)
1877 .unwrap();
1878 let hs_req = reply_frame(&mut server, &hs, false).await;
1879 assert_request_header(
1880 &hs_req,
1881 ApiKey(API_KEY_SASL_HANDSHAKE),
1882 ApiVersion(1),
1883 MIN_RESERVED_CORRELATION_ID + 1,
1884 false,
1885 );
1886 let mut hs_body = request_body(&hs_req, false);
1887 let hs_decoded = SaslHandshakeRequest::decode(&mut hs_body, 1).unwrap();
1888 assert_eq!(hs_decoded.mechanism, "SCRAM-SHA-256");
1889
1890 let mut au = BytesMut::new();
1891 SaslAuthenticateResponse {
1892 error_code: SASL_AUTHENTICATION_FAILED,
1893 error_message: Some("nope".into()),
1894 ..Default::default()
1895 }
1896 .encode(&mut au, 2)
1897 .unwrap();
1898 let au_req = reply_frame(&mut server, &au, true).await;
1899 let _ = decode_sasl_authenticate_frame!(au_req, MIN_RESERVED_CORRELATION_ID + 2);
1900 });
1901
1902 let creds = SaslCredentials::Scram {
1903 mechanism: SaslMechanism::ScramSha256,
1904 username: "u".into(),
1905 password: "p".into(),
1906 delegation_token: false,
1907 };
1908 let err = outbound_sasl(
1909 &mut client,
1910 &creds,
1911 "localhost",
1912 TEST_CLIENT_ID,
1913 ClientFrameMax::default(),
1914 )
1915 .await
1916 .unwrap_err();
1917 assert2::assert!(
1918 let OutboundSaslError::Authentication(SaslAuthenticationError::Failed(msg)) = err
1919 );
1920 check!(msg.contains("round 1"));
1921 timeout(Duration::from_secs(1), server_task)
1922 .await
1923 .expect("server observed SCRAM first round")
1924 .unwrap();
1925 }
1926
1927 #[tokio::test]
1928 async fn outbound_scram_rejects_broker_error_on_second_round() {
1929 let (mut client, mut server) = duplex(8192);
1930 let server_task = tokio::spawn(async move {
1931 answer_api_versions(&mut server).await;
1932 let mut hs = BytesMut::new();
1933 SaslHandshakeResponse {
1934 error_code: 0,
1935 ..Default::default()
1936 }
1937 .encode(&mut hs, 1)
1938 .unwrap();
1939 let hs_req = reply_frame(&mut server, &hs, false).await;
1940 assert_request_header(
1941 &hs_req,
1942 ApiKey(API_KEY_SASL_HANDSHAKE),
1943 ApiVersion(1),
1944 MIN_RESERVED_CORRELATION_ID + 1,
1945 false,
1946 );
1947
1948 let cred = hash_scram_password(b"p", SaslMechanism::ScramSha256, 4096);
1949 let scram_server = ScramServerExchange::new("u".to_owned(), cred);
1950
1951 let first_req_len = server.read_u32().await.unwrap();
1952 let mut first_req = vec![0u8; first_req_len as usize];
1953 server.read_exact(&mut first_req).await.unwrap();
1954 let first_auth =
1955 decode_sasl_authenticate_frame!(first_req, MIN_RESERVED_CORRELATION_ID + 2);
1956 let server_first = match scram_server.step(&first_auth.auth_bytes) {
1957 StepResult::Continue(bytes, _next) => bytes,
1958 other => panic!("server first SCRAM step must continue, got {other:?}"),
1959 };
1960
1961 let mut first_resp = BytesMut::new();
1962 SaslAuthenticateResponse {
1963 error_code: 0,
1964 auth_bytes: bytes::Bytes::from(server_first),
1965 ..Default::default()
1966 }
1967 .encode(&mut first_resp, 2)
1968 .unwrap();
1969 write_response_frame(
1970 &mut server,
1971 MIN_RESERVED_CORRELATION_ID + 2,
1972 &first_resp,
1973 true,
1974 )
1975 .await;
1976
1977 let mut second_resp = BytesMut::new();
1978 SaslAuthenticateResponse {
1979 error_code: SASL_AUTHENTICATION_FAILED,
1980 error_message: Some("second nope".into()),
1981 ..Default::default()
1982 }
1983 .encode(&mut second_resp, 2)
1984 .unwrap();
1985 let second_req = reply_frame(&mut server, &second_resp, true).await;
1986 let _ = decode_sasl_authenticate_frame!(second_req, MIN_RESERVED_CORRELATION_ID + 3);
1987 });
1988
1989 let creds = SaslCredentials::Scram {
1990 mechanism: SaslMechanism::ScramSha256,
1991 username: "u".into(),
1992 password: "p".into(),
1993 delegation_token: false,
1994 };
1995 let err = outbound_sasl(
1996 &mut client,
1997 &creds,
1998 "localhost",
1999 TEST_CLIENT_ID,
2000 ClientFrameMax::default(),
2001 )
2002 .await
2003 .unwrap_err();
2004 assert2::assert!(
2005 let OutboundSaslError::Authentication(SaslAuthenticationError::Failed(msg)) = err
2006 );
2007 check!(msg.contains("round 2"));
2008 timeout(Duration::from_secs(1), server_task)
2009 .await
2010 .expect("server observed SCRAM second round")
2011 .unwrap();
2012 }
2013
2014 #[tokio::test]
2015 async fn round_trip_rejects_response_without_correlation_id() {
2016 let (mut client, mut server) = duplex(8192);
2017 let server_task = tokio::spawn(async move {
2018 let req_len = server.read_u32().await.unwrap();
2019 let mut req = vec![0u8; req_len as usize];
2020 server.read_exact(&mut req).await.unwrap();
2021 server.write_u32(3).await.unwrap();
2022 server.write_all(&[1, 2, 3]).await.unwrap();
2023 server.flush().await.unwrap();
2024 });
2025
2026 let err = round_trip(
2027 &mut client,
2028 ApiKey(API_KEY_SASL_HANDSHAKE),
2029 ApiVersion(1),
2030 99,
2031 false,
2032 &[],
2033 SaslPolicy {
2034 client_id: TEST_CLIENT_ID,
2035 frame_max: ClientFrameMax::default(),
2036 authenticate_version: Some(2),
2037 session_lifetime_ms: &AtomicI64::new(0),
2038 },
2039 )
2040 .await
2041 .unwrap_err();
2042 check!(matches!(
2043 err,
2044 OutboundSaslError::Authentication(SaslAuthenticationError::IllegalState(msg))
2045 if msg == "response missing corr_id"
2046 ));
2047 server_task.await.unwrap();
2048 }
2049
2050 #[tokio::test]
2051 async fn round_trip_rejects_flexible_response_without_tagged_fields_byte() {
2052 let (mut client, mut server) = duplex(8192);
2053 let server_task = tokio::spawn(async move {
2054 let req_len = server.read_u32().await.unwrap();
2055 let mut req = vec![0u8; req_len as usize];
2056 server.read_exact(&mut req).await.unwrap();
2057 server.write_u32(4).await.unwrap();
2058 server.write_i32(99).await.unwrap();
2059 server.flush().await.unwrap();
2060 });
2061
2062 let err = round_trip(
2063 &mut client,
2064 ApiKey(API_KEY_SASL_AUTHENTICATE),
2065 ApiVersion(2),
2066 99,
2067 true,
2068 &[],
2069 SaslPolicy {
2070 client_id: TEST_CLIENT_ID,
2071 frame_max: ClientFrameMax::default(),
2072 authenticate_version: Some(2),
2073 session_lifetime_ms: &AtomicI64::new(0),
2074 },
2075 )
2076 .await
2077 .unwrap_err();
2078 check!(matches!(
2079 err,
2080 OutboundSaslError::Authentication(SaslAuthenticationError::IllegalState(msg))
2081 if msg == "flexible response missing tagged-fields byte"
2082 ));
2083 server_task.await.unwrap();
2084 }
2085
2086 #[tokio::test]
2087 async fn round_trip_uses_the_configured_client_id() {
2088 let (mut client, mut server) = duplex(8192);
2089 let server_task = tokio::spawn(async move {
2090 let req = reply_frame(&mut server, &[], false).await;
2091 let client_len = usize::try_from(i16::from_be_bytes([req[8], req[9]])).unwrap();
2092 assert_eq!(&req[10..10 + client_len], b"configured-sasl-client");
2093 });
2094
2095 round_trip(
2096 &mut client,
2097 ApiKey(API_KEY_SASL_HANDSHAKE),
2098 ApiVersion(1),
2099 99,
2100 false,
2101 &[],
2102 SaslPolicy {
2103 client_id: TEST_CLIENT_ID,
2104 frame_max: ClientFrameMax::default(),
2105 authenticate_version: Some(2),
2106 session_lifetime_ms: &AtomicI64::new(0),
2107 },
2108 )
2109 .await
2110 .unwrap();
2111 server_task.await.unwrap();
2112 }
2113
2114 #[tokio::test]
2115 async fn round_trip_rejects_an_oversized_request_before_writing() {
2116 let (mut client, _server) = duplex(8192);
2117 let error = round_trip(
2118 &mut client,
2119 ApiKey(API_KEY_SASL_HANDSHAKE),
2120 ApiVersion(1),
2121 99,
2122 false,
2123 &[0],
2124 SaslPolicy {
2125 client_id: "",
2126 frame_max: ClientFrameMax::try_from(krabka_units::bytes(10)).unwrap(),
2127 authenticate_version: Some(2),
2128 session_lifetime_ms: &AtomicI64::new(0),
2129 },
2130 )
2131 .await
2132 .unwrap_err();
2133
2134 assert!(error.to_string().contains("encoded 11"));
2135 assert!(error.to_string().contains("maximum 10"));
2136 }
2137
2138 #[tokio::test]
2139 async fn round_trip_rejects_an_oversized_response_before_allocating() {
2140 let (mut client, mut server) = duplex(8192);
2141 let server_task = tokio::spawn(async move {
2142 let request_len = server.read_u32().await.unwrap();
2143 let mut request = vec![0; request_len as usize];
2144 server.read_exact(&mut request).await.unwrap();
2145 server.write_u32(17).await.unwrap();
2146 server.flush().await.unwrap();
2147 });
2148
2149 let error = round_trip(
2150 &mut client,
2151 ApiKey(API_KEY_SASL_HANDSHAKE),
2152 ApiVersion(1),
2153 99,
2154 false,
2155 &[],
2156 SaslPolicy {
2157 client_id: "",
2158 frame_max: ClientFrameMax::try_from(krabka_units::bytes(16)).unwrap(),
2159 authenticate_version: Some(2),
2160 session_lifetime_ms: &AtomicI64::new(0),
2161 },
2162 )
2163 .await
2164 .unwrap_err();
2165
2166 assert!(error.to_string().contains("announced 17"));
2167 assert!(error.to_string().contains("maximum 16"));
2168 server_task.await.unwrap();
2169 }
2170
2171 #[derive(Debug, PartialEq, Eq)]
2173 enum Verdict {
2174 Ok,
2175 Rejected(SaslAuthenticationError),
2176 Server(i16),
2177 Other(String),
2178 }
2179
2180 fn verdict(result: Result<(), OutboundSaslError>) -> Verdict {
2181 match result {
2182 Ok(()) => Verdict::Ok,
2183 Err(OutboundSaslError::Authentication(error)) => Verdict::Rejected(error),
2184 Err(OutboundSaslError::Server { error_code, .. }) => Verdict::Server(error_code),
2185 Err(error) => Verdict::Other(error.to_string()),
2186 }
2187 }
2188
2189 #[test]
2194 fn handshake_error_codes_map_to_kafka_authentication_exceptions() {
2195 let enabled = "enabled mechanisms are [\"GSSAPI\"]";
2196 for (error_code, expected) in [
2197 (0, Verdict::Ok),
2198 (
2199 UNSUPPORTED_SASL_MECHANISM,
2200 Verdict::Rejected(SaslAuthenticationError::UnsupportedMechanism(format!(
2201 "client SASL mechanism 'PLAIN' not enabled in the server, {enabled}"
2202 ))),
2203 ),
2204 (
2205 ILLEGAL_SASL_STATE,
2206 Verdict::Rejected(SaslAuthenticationError::IllegalState(format!(
2207 "unexpected handshake request with client mechanism PLAIN, {enabled}"
2208 ))),
2209 ),
2210 (
2211 87,
2212 Verdict::Rejected(SaslAuthenticationError::IllegalState(format!(
2213 "unknown error code 87, client mechanism is PLAIN, {enabled}"
2214 ))),
2215 ),
2216 ] {
2217 let response = SaslHandshakeResponse {
2218 error_code,
2219 mechanisms: vec!["GSSAPI".into()],
2220 ..Default::default()
2221 };
2222 check!(
2223 verdict(handshake_result(SaslMechanism::Plain, &response)) == expected,
2224 "error_code {error_code}"
2225 );
2226 }
2227 }
2228
2229 #[test]
2233 fn authenticate_error_codes_map_to_kafka_exceptions() {
2234 let message = |code: i16| {
2235 format!("SaslAuthenticate(PLAIN) error_code={code} error_message=Some(\"no\")")
2236 };
2237 for (error_code, expected) in [
2238 (
2239 UNSUPPORTED_SASL_MECHANISM,
2240 Verdict::Rejected(SaslAuthenticationError::UnsupportedMechanism(message(33))),
2241 ),
2242 (
2243 ILLEGAL_SASL_STATE,
2244 Verdict::Rejected(SaslAuthenticationError::IllegalState(message(34))),
2245 ),
2246 (
2247 SASL_AUTHENTICATION_FAILED,
2248 Verdict::Rejected(SaslAuthenticationError::Failed(message(58))),
2249 ),
2250 (35, Verdict::Server(35)),
2251 (-1, Verdict::Server(-1)),
2252 ] {
2253 let response = SaslAuthenticateResponse {
2254 error_code,
2255 error_message: Some("no".into()),
2256 ..Default::default()
2257 };
2258 check!(
2259 verdict(Err(authenticate_error("PLAIN", &response))) == expected,
2260 "error_code {error_code}"
2261 );
2262 }
2263 }
2264
2265 fn assert_request_header(
2266 req: &[u8],
2267 api_key: ApiKey,
2268 api_version: ApiVersion,
2269 corr_id: i32,
2270 flexible: bool,
2271 ) {
2272 check!(ApiKey(i16::from_be_bytes([req[0], req[1]])) == api_key);
2273 check!(ApiVersion(i16::from_be_bytes([req[2], req[3]])) == api_version);
2274 check!(i32::from_be_bytes([req[4], req[5], req[6], req[7]]) == corr_id);
2275 let client_len = i16::from_be_bytes([req[8], req[9]]);
2276 assert_eq!(client_len, i16::try_from(TEST_CLIENT_ID.len()).unwrap());
2277 assert_eq!(
2278 &req[10..10 + TEST_CLIENT_ID.len()],
2279 TEST_CLIENT_ID.as_bytes()
2280 );
2281 if flexible {
2282 assert_eq!(req[10 + TEST_CLIENT_ID.len()], 0);
2283 }
2284 }
2285
2286 fn request_body(req: &[u8], flexible: bool) -> &[u8] {
2287 let header_len = 10 + TEST_CLIENT_ID.len() + usize::from(flexible);
2288 &req[header_len..]
2289 }
2290
2291 async fn write_response_frame<S>(stream: &mut S, corr_id: i32, body: &[u8], flex_header: bool)
2292 where
2293 S: AsyncWrite + Unpin,
2294 {
2295 let mut frame = BytesMut::new();
2296 frame.put_i32(corr_id);
2297 if flex_header {
2298 frame.put_u8(0);
2299 }
2300 frame.put_slice(body);
2301 stream
2302 .write_u32(u32::try_from(frame.len()).unwrap())
2303 .await
2304 .unwrap();
2305 stream.write_all(&frame).await.unwrap();
2306 stream.flush().await.unwrap();
2307 }
2308}