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..1bee25510 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(cfg(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(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 { 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..7ca012ef1 100644 --- a/src/client/config.rs +++ b/src/client/config.rs @@ -310,10 +310,18 @@ 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), + _ => 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)? => { Ok(AuthMethod::Integrated) } + _ => 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..6e892d767 100644 --- a/src/error.rs +++ b/src/error.rs @@ -48,6 +48,15 @@ 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), #[error( "Server requested a connection to an alternative address: `{}:{}`", host, @@ -83,7 +92,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 +166,14 @@ impl From for Error { Error::Gssapi(format!("{}", err)) } } + +#[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)) + } +} \ 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;