From beeaf2c401d639b457f9fa8f0b246979aba76a95 Mon Sep 17 00:00:00 2001 From: Carl Voller <27472988+carlvoller@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:21:38 +0800 Subject: [PATCH 1/4] feat: added sspi ntlm without kerberos using sspi-rs --- Cargo.toml | 2 + src/client/auth.rs | 25 ++++++--- src/client/config.rs | 2 + src/client/connection.rs | 87 ++++++++++++++++++++++++++++++- src/error.rs | 12 ++++- src/tds/codec/login.rs | 2 +- src/tds/codec/token/token_sspi.rs | 2 +- src/tds/context.rs | 2 +- src/tds/stream/token.rs | 2 +- 9 files changed, 121 insertions(+), 15 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0caaac815..1dabc41b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,7 @@ winauth = { version = "0.0.4", optional = true } [target.'cfg(unix)'.dependencies] libgssapi = { version = "0.8.1", optional = true, default-features = false } +sspi = { version = "0.18", optional = true } [dependencies.async-native-tls] version = "0.4" @@ -202,3 +203,4 @@ bigdecimal = ["bigdecimal_"] rustls = ["tokio-rustls", "tokio-util", "rustls-pemfile", "rustls-native-certs"] native-tls = ["async-native-tls"] vendored-openssl = ["opentls"] +sspi-rs = ["sspi"] \ No newline at end of file diff --git a/src/client/auth.rs b/src/client/auth.rs index 208d8d060..018dda844 100644 --- a/src/client/auth.rs +++ b/src/client/auth.rs @@ -26,16 +26,22 @@ impl Debug for SqlServerAuth { } #[derive(Clone, PartialEq, Eq)] -#[cfg(any(all(windows, feature = "winauth"), doc))] -#[cfg_attr(feature = "docs", doc(all(windows, feature = "winauth")))] +#[cfg(any(all(windows, feature = "winauth"), all(unix, feature = "sspi-rs"), doc))] +#[cfg_attr( + feature = "docs", + doc(any(all(windows, feature = "winauth"), all(unix, feature = "sspi-rs"))) +)] pub struct WindowsAuth { pub(crate) user: String, pub(crate) password: String, pub(crate) domain: Option, } -#[cfg(any(all(windows, feature = "winauth"), doc))] -#[cfg_attr(feature = "docs", doc(all(windows, feature = "winauth")))] +#[cfg(any(all(windows, feature = "winauth"), all(unix, feature = "sspi-rs"), doc))] +#[cfg_attr( + feature = "docs", + doc(any(all(windows, feature = "winauth"), all(unix, feature = "sspi-rs"))) +)] impl Debug for WindowsAuth { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("WindowsAuth") @@ -52,8 +58,11 @@ pub enum AuthMethod { /// Authenticate directly with SQL Server. SqlServer(SqlServerAuth), /// Authenticate with Windows credentials. - #[cfg(any(all(windows, feature = "winauth"), doc))] - #[cfg_attr(feature = "docs", doc(cfg(all(windows, feature = "winauth"))))] + #[cfg(any(all(windows, feature = "winauth"), all(unix, feature = "sspi-rs"), doc))] + #[cfg_attr( + feature = "docs", + doc(any(all(windows, feature = "winauth"), all(unix, feature = "sspi-rs"))) + )] Windows(WindowsAuth), /// Authenticate as the currently logged in user. On Windows uses SSPI and /// Kerberos on Unix platforms. @@ -84,8 +93,8 @@ impl AuthMethod { } /// Construct a new Windows authentication configuration. - #[cfg(any(all(windows, feature = "winauth"), doc))] - #[cfg_attr(feature = "docs", doc(cfg(all(windows, feature = "winauth"))))] + #[cfg(any(all(windows, feature = "winauth"), all(unix, feature = "sspi-rs"), doc))] + #[cfg_attr(feature = "docs", doc(any(all(windows, feature = "winauth"), all(unix, feature = "sspi-rs"))))] pub fn windows(user: impl AsRef, password: impl ToString) -> Self { let (domain, user) = match user.as_ref().find('\\') { Some(idx) => (Some(&user.as_ref()[..idx]), &user.as_ref()[idx + 1..]), diff --git a/src/client/config.rs b/src/client/config.rs index fff68bc15..ea2f2d3e1 100644 --- a/src/client/config.rs +++ b/src/client/config.rs @@ -314,6 +314,8 @@ pub(crate) trait ConfigString { Some(val) if val.to_lowercase() == "sspi" || Self::parse_bool(val)? => { Ok(AuthMethod::Integrated) } + + // Should sspi-rs take over the default behaviour here if enabled? _ => Ok(AuthMethod::sql_server(user.unwrap_or(""), pw.unwrap_or(""))), } } diff --git a/src/client/connection.rs b/src/client/connection.rs index 09d372561..77932e12a 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -18,7 +18,7 @@ use crate::{ }; use asynchronous_codec::Framed; use bytes::BytesMut; -#[cfg(any(windows, feature = "integrated-auth-gssapi"))] +#[cfg(any(windows, feature = "integrated-auth-gssapi", feature = "sspi-rs"))] use codec::TokenSspi; use futures_util::io::{AsyncRead, AsyncWrite}; use futures_util::ready; @@ -40,6 +40,12 @@ use tracing::{event, Level}; #[cfg(all(windows, feature = "winauth"))] use winauth::{windows::NtlmSspiBuilder, NextBytes}; +#[cfg(all(unix, feature = "sspi-rs"))] +use sspi::{ + builders::EmptyInitializeSecurityContext, AuthIdentity, BufferType, ClientRequestFlags, + CredentialUse, DataRepresentation, Ntlm, SecurityBuffer, Sspi, SspiImpl, Username, +}; + /// A `Connection` is an abstraction between the [`Client`] and the server. It /// can be used as a `Stream` to fetch [`Packet`]s from and to `send` packets /// splitting them to the negotiated limit automatically. @@ -120,7 +126,7 @@ impl Connection { TokenStream::new(self).flush_done().await } - #[cfg(any(windows, feature = "integrated-auth-gssapi"))] + #[cfg(any(windows, feature = "integrated-auth-gssapi", feature = "sspi-rs"))] /// Flush the incoming token stream until receiving `SSPI` token. async fn flush_sspi(&mut self) -> crate::Result { TokenStream::new(self).flush_sspi().await @@ -381,6 +387,83 @@ impl Connection { self.send(header, next_token).await?; } + + #[cfg(all(unix, feature = "sspi-rs", not(all(windows, feature = "winauth"))))] + AuthMethod::Windows(auth) => { + let mut ntlm = Ntlm::new(); + + let username = Username::new(&auth.user, auth.domain.as_deref()) + .map_err(|e| sspi::Error::from(e))?; + + let identity = AuthIdentity { + username, + password: auth.password.clone().into(), + }; + + let mut creds = ntlm + .acquire_credentials_handle() + .with_credential_use(CredentialUse::Outbound) + .with_auth_data(&identity) + .execute(&mut ntlm)?; + + let spn = self.context.spn().to_string(); + + let mut input = vec![SecurityBuffer::new(Vec::new(), BufferType::Token)]; + let mut output = vec![SecurityBuffer::new(Vec::new(), BufferType::Token)]; + + let mut builder = ntlm + .initialize_security_context() + .with_credentials_handle(&mut creds.credentials_handle) + .with_context_requirements( + ClientRequestFlags::CONFIDENTIALITY | ClientRequestFlags::ALLOCATE_MEMORY, + ) + .with_target_data_representation(DataRepresentation::Native) + .with_target_name(&spn) + .with_input(&mut input) + .with_output(&mut output); + + let _ = ntlm + .initialize_security_context_impl(&mut builder)? + .resolve_to_result()?; + + login_message.integrated_security(Some(output[0].buffer.clone())); + let id = self.context.next_packet_id(); + self.send(PacketHeader::login(id), login_message).await?; + self = self.post_login_encryption(encryption); + + let sspi_bytes = self.flush_sspi().await?; + + let mut input = vec![SecurityBuffer::new( + sspi_bytes.as_ref().to_vec(), + BufferType::Token, + )]; + let mut output = vec![SecurityBuffer::new(Vec::new(), BufferType::Token)]; + + let mut builder = ntlm + .initialize_security_context() + .with_credentials_handle(&mut creds.credentials_handle) + .with_context_requirements( + ClientRequestFlags::CONFIDENTIALITY | ClientRequestFlags::ALLOCATE_MEMORY, + ) + .with_target_data_representation(DataRepresentation::Native) + .with_target_name(&spn) + .with_input(&mut input) + .with_output(&mut output); + + let _ = ntlm + .initialize_security_context_impl(&mut builder)? + .resolve_to_result()?; + + event!(Level::TRACE, authenticate_len = output[0].buffer.len()); + + let id = self.context.next_packet_id(); + self.send( + PacketHeader::login(id), + TokenSspi::new(output[0].buffer.clone()), + ) + .await?; + } + #[cfg(all(windows, feature = "winauth"))] AuthMethod::Windows(auth) => { let spn = self.context.spn().to_string(); diff --git a/src/error.rs b/src/error.rs index 98bf01b58..6c25b52e7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -48,6 +48,9 @@ pub enum Error { /// An error from the GSSAPI library. #[error("GSSAPI Error: {}", _0)] Gssapi(String), + /// An error in the sspi-rs library. + #[error("sspi-rs Error {}", _0)] + SspiRs(String), #[error( "Server requested a connection to an alternative address: `{}:{}`", host, @@ -83,7 +86,7 @@ impl Error { impl From for Error { fn from(e: uuid::Error) -> Self { - Self::Conversion(format!("Error convertiong a Guid value {}", e).into()) + Self::Conversion(format!("Error converting a Guid value {}", e).into()) } } @@ -157,3 +160,10 @@ impl From for Error { Error::Gssapi(format!("{}", err)) } } + +#[cfg(all(unix, feature = "sspi-rs"))] +impl From for Error { + fn from(err: sspi::Error) -> Self { + Error::SspiRs(format!("{}", err)) + } +} \ No newline at end of file diff --git a/src/tds/codec/login.rs b/src/tds/codec/login.rs index 265db381e..91b4b0a5f 100644 --- a/src/tds/codec/login.rs +++ b/src/tds/codec/login.rs @@ -187,7 +187,7 @@ impl<'a> LoginMessage<'a> { } } - #[cfg(any(all(unix, feature = "integrated-auth-gssapi"), windows))] + #[cfg(any(all(unix, any(feature = "integrated-auth-gssapi", feature = "sspi-rs")), windows))] pub fn integrated_security(&mut self, bytes: Option>) { if bytes.is_some() { self.option_flags_2.insert(OptionFlag2::IntegratedSecurity); diff --git a/src/tds/codec/token/token_sspi.rs b/src/tds/codec/token/token_sspi.rs index 954d6dd8b..c6239a22a 100644 --- a/src/tds/codec/token/token_sspi.rs +++ b/src/tds/codec/token/token_sspi.rs @@ -12,7 +12,7 @@ impl AsRef<[u8]> for TokenSspi { } impl TokenSspi { - #[cfg(any(windows, all(unix, feature = "integrated-auth-gssapi")))] + #[cfg(any(windows, all(unix, any(feature = "integrated-auth-gssapi", feature = "sspi-rs"))))] pub fn new(bytes: Vec) -> Self { Self(bytes) } diff --git a/src/tds/context.rs b/src/tds/context.rs index 732bac15c..4d88d771a 100644 --- a/src/tds/context.rs +++ b/src/tds/context.rs @@ -62,7 +62,7 @@ impl Context { self.spn = Some(format!("MSSQLSvc/{}:{}", host.as_ref(), port)); } - #[cfg(any(windows, all(unix, feature = "integrated-auth-gssapi")))] + #[cfg(any(windows, all(unix, any(feature = "integrated-auth-gssapi", feature = "sspi-rs"))))] pub fn spn(&self) -> &str { self.spn.as_deref().unwrap_or("") } diff --git a/src/tds/stream/token.rs b/src/tds/stream/token.rs index 35ce0658b..3b23b8798 100644 --- a/src/tds/stream/token.rs +++ b/src/tds/stream/token.rs @@ -75,7 +75,7 @@ where } } - #[cfg(any(windows, feature = "integrated-auth-gssapi"))] + #[cfg(any(windows, feature = "integrated-auth-gssapi", feature = "sspi-rs"))] pub(crate) async fn flush_sspi(self) -> crate::Result { let mut stream = self.try_unfold(); let mut last_error = None; From 9fc93bcbcdf11208c6210c1a9a3a5d3c268c4159 Mon Sep 17 00:00:00 2001 From: Carl Voller <27472988+carlvoller@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:58:50 +0800 Subject: [PATCH 2/4] fix: added cfg to Error::SspiRs, fixed doc for WindowsAuth, added new sspi-rs parsing in ConfigString --- src/client/auth.rs | 4 ++-- src/client/config.rs | 12 ++++++++++-- src/error.rs | 6 ++++++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/client/auth.rs b/src/client/auth.rs index 018dda844..1bee25510 100644 --- a/src/client/auth.rs +++ b/src/client/auth.rs @@ -29,7 +29,7 @@ impl Debug for SqlServerAuth { #[cfg(any(all(windows, feature = "winauth"), all(unix, feature = "sspi-rs"), doc))] #[cfg_attr( feature = "docs", - doc(any(all(windows, feature = "winauth"), all(unix, feature = "sspi-rs"))) + doc(cfg(any(all(windows, feature = "winauth"), all(unix, feature = "sspi-rs")))) )] pub struct WindowsAuth { pub(crate) user: String, @@ -40,7 +40,7 @@ pub struct WindowsAuth { #[cfg(any(all(windows, feature = "winauth"), all(unix, feature = "sspi-rs"), doc))] #[cfg_attr( feature = "docs", - doc(any(all(windows, feature = "winauth"), all(unix, feature = "sspi-rs"))) + doc(cfg(any(all(windows, feature = "winauth"), all(unix, feature = "sspi-rs")))) )] impl Debug for WindowsAuth { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { diff --git a/src/client/config.rs b/src/client/config.rs index ea2f2d3e1..82cd7ffd4 100644 --- a/src/client/config.rs +++ b/src/client/config.rs @@ -310,12 +310,20 @@ pub(crate) trait ConfigString { (None, None) => Ok(AuthMethod::Integrated), _ => Ok(AuthMethod::windows(user.unwrap_or(""), pw.unwrap_or(""))), }, - #[cfg(feature = "integrated-auth-gssapi")] + #[cfg(all(unix, feature = "sspi-rs"))] + Some(val) if val.to_lowercase() == "sspi" || Self::parse_bool(val)? => match (user, pw) { + (Some(user), Some(pw)) => Ok(AuthMethod::windows(user, pw)), + #[cfg(feature = "integrated-auth-gssapi")] + (None, None) => Ok(AuthMethod::Integrated), + // this maintains the existing default behaviour. we could also throw an error here too, + // but that may break some edge case an existing user may be relying on + _ => Ok(AuthMethod::sql_server("", "")), + }, + #[cfg(all(unix, feature = "integrated-auth-gssapi", not(feature = "sspi-rs")))] Some(val) if val.to_lowercase() == "sspi" || Self::parse_bool(val)? => { Ok(AuthMethod::Integrated) } - // Should sspi-rs take over the default behaviour here if enabled? _ => Ok(AuthMethod::sql_server(user.unwrap_or(""), pw.unwrap_or(""))), } } diff --git a/src/error.rs b/src/error.rs index 6c25b52e7..195da9074 100644 --- a/src/error.rs +++ b/src/error.rs @@ -48,6 +48,12 @@ pub enum Error { /// An error from the GSSAPI library. #[error("GSSAPI Error: {}", _0)] Gssapi(String), + + #[cfg(any(all(unix, feature = "sspi-rs"), doc))] + #[cfg_attr( + feature = "docs", + doc(cfg(all(unix, feature = "sspi-rs"))) + )] /// An error in the sspi-rs library. #[error("sspi-rs Error {}", _0)] SspiRs(String), From 5ab7325057f5a3b5ce53ef8fc47bdefd5ca8a567 Mon Sep 17 00:00:00 2001 From: Carl Voller <27472988+carlvoller@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:11:46 +0800 Subject: [PATCH 3/4] fix: removed regression in ConfigString, fixed error formatting --- src/client/config.rs | 2 +- src/error.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/config.rs b/src/client/config.rs index 82cd7ffd4..8629d300c 100644 --- a/src/client/config.rs +++ b/src/client/config.rs @@ -317,7 +317,7 @@ pub(crate) trait ConfigString { (None, None) => Ok(AuthMethod::Integrated), // this maintains the existing default behaviour. we could also throw an error here too, // but that may break some edge case an existing user may be relying on - _ => Ok(AuthMethod::sql_server("", "")), + _ => Ok(AuthMethod::sql_server(user.unwrap_or(""), pw.unwrap_or(""))), }, #[cfg(all(unix, feature = "integrated-auth-gssapi", not(feature = "sspi-rs")))] Some(val) if val.to_lowercase() == "sspi" || Self::parse_bool(val)? => { diff --git a/src/error.rs b/src/error.rs index 195da9074..a28523e77 100644 --- a/src/error.rs +++ b/src/error.rs @@ -55,7 +55,7 @@ pub enum Error { doc(cfg(all(unix, feature = "sspi-rs"))) )] /// An error in the sspi-rs library. - #[error("sspi-rs Error {}", _0)] + #[error("sspi-rs Error: {}", _0)] SspiRs(String), #[error( "Server requested a connection to an alternative address: `{}:{}`", @@ -168,7 +168,7 @@ impl From for Error { } #[cfg(all(unix, feature = "sspi-rs"))] -impl From for Error { +impl From for Error { fn from(err: sspi::Error) -> Self { Error::SspiRs(format!("{}", err)) } From 965a66d7871e1439b3cc67d02572f3e3dc71a716 Mon Sep 17 00:00:00 2001 From: Carl Voller <27472988+carlvoller@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:20:50 +0800 Subject: [PATCH 4/4] fix: added docs to sspi::Error, fixed alignment issues with windows and unix sspi behaviours in ConfigString --- src/client/config.rs | 4 +--- src/error.rs | 4 ++++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/client/config.rs b/src/client/config.rs index 8629d300c..7ca012ef1 100644 --- a/src/client/config.rs +++ b/src/client/config.rs @@ -315,9 +315,7 @@ pub(crate) trait ConfigString { (Some(user), Some(pw)) => Ok(AuthMethod::windows(user, pw)), #[cfg(feature = "integrated-auth-gssapi")] (None, None) => Ok(AuthMethod::Integrated), - // this maintains the existing default behaviour. we could also throw an error here too, - // but that may break some edge case an existing user may be relying on - _ => Ok(AuthMethod::sql_server(user.unwrap_or(""), pw.unwrap_or(""))), + _ => Ok(AuthMethod::windows(user.unwrap_or(""), pw.unwrap_or(""))), }, #[cfg(all(unix, feature = "integrated-auth-gssapi", not(feature = "sspi-rs")))] Some(val) if val.to_lowercase() == "sspi" || Self::parse_bool(val)? => { diff --git a/src/error.rs b/src/error.rs index a28523e77..6e892d767 100644 --- a/src/error.rs +++ b/src/error.rs @@ -168,6 +168,10 @@ impl From for Error { } #[cfg(all(unix, feature = "sspi-rs"))] +#[cfg_attr( + feature = "docs", + doc(cfg(all(unix, feature = "sspi-rs"))) +)] impl From for Error { fn from(err: sspi::Error) -> Self { Error::SspiRs(format!("{}", err))