Skip to content

Commit

Permalink
Initial NTLM authentication support
Browse files Browse the repository at this point in the history
Add NTLM authentication support via sspi-rs, using GSS-SPNEGO SASL
binds with no actual negotiation, by providing the NTLMSSP NEGOTIATE
token in the initial request. Limitations:

* Placed behind a feature flag, since sspi brings in a great number
  of additional dependencies.

* NTLM sealing not supported on ldap:// connections.

* Channel bindings currently not supported on ldaps:// connections.

The implementation in ldap3-proto was of great help.

Closes #32
  • Loading branch information
inejge committed May 13, 2024
1 parent cb31bf2 commit 4ba1239
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 6 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ rustls-native-certs = { version = "0.7.0", optional = true }
x509-parser = { version = "0.16.0", optional = true }
ring = { version = "0.17.7", optional = true }
cross-krb5 = { version = "0.4.0", optional = true }
sspi = { version = "0.12.0", optional = true }
async-trait = "0.1.60"

[dependencies.lber]
Expand All @@ -45,6 +46,7 @@ tls-native = ["dep:native-tls", "dep:tokio-native-tls", "tokio/rt"]
tls-rustls = ["dep:rustls", "dep:tokio-rustls", "dep:rustls-native-certs", "dep:x509-parser", "dep:ring", "tokio/rt"]
sync = ["tokio/rt"]
gssapi = ["cross-krb5"]
ntlm = ["sspi"]

[dev-dependencies]
tokio = { version = "1", features = ["macros", "io-util", "sync", "time", "net", "rt-multi-thread"] }
Expand Down
8 changes: 4 additions & 4 deletions src/conn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,7 @@ impl LdapConnAsync {
} else {
panic!("underlying stream not TCP");
};
#[cfg(feature = "gssapi")]
#[cfg(any(feature = "gssapi", feature = "ntlm"))]
{
ldap.tls_endpoint_token =
Arc::new(LdapConnAsync::get_tls_endpoint_token(&tls_stream));
Expand Down Expand Up @@ -553,7 +553,7 @@ impl LdapConnAsync {
builder.build().expect("connector")
}

#[cfg(all(feature = "gssapi", feature = "tls-native"))]
#[cfg(all(any(feature = "gssapi", feature = "ntlm"), feature = "tls-native"))]
fn get_tls_endpoint_token(s: &TlsStream<TcpStream>) -> Option<Vec<u8>> {
match s.get_ref().tls_server_end_point() {
Ok(ep) => {
Expand All @@ -569,7 +569,7 @@ impl LdapConnAsync {
}
}

#[cfg(all(feature = "gssapi", feature = "tls-rustls"))]
#[cfg(all(any(feature = "gssapi", feature = "ntlm"), feature = "tls-rustls"))]
fn get_tls_endpoint_token(s: &TlsStream<TcpStream>) -> Option<Vec<u8>> {
use x509_parser::prelude::*;

Expand Down Expand Up @@ -629,7 +629,7 @@ impl LdapConnAsync {
sasl_param,
#[cfg(feature = "gssapi")]
client_ctx,
#[cfg(feature = "gssapi")]
#[cfg(any(feature = "gssapi", feature = "ntlm"))]
tls_endpoint_token: Arc::new(None),
has_tls: false,
last_id: 0,
Expand Down
87 changes: 85 additions & 2 deletions src/ldap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ pub struct Ldap {
pub(crate) sasl_param: Arc<RwLock<(bool, u32)>>, // sasl_wrap, sasl_max_send
#[cfg(feature = "gssapi")]
pub(crate) client_ctx: Arc<Mutex<Option<ClientCtx>>>,
#[cfg(feature = "gssapi")]
#[cfg(any(feature = "gssapi", feature = "ntlm"))]
pub(crate) tls_endpoint_token: Arc<Option<Vec<u8>>>,
pub(crate) has_tls: bool,
pub timeout: Option<Duration>,
Expand All @@ -102,7 +102,7 @@ impl Clone for Ldap {
sasl_param: self.sasl_param.clone(),
#[cfg(feature = "gssapi")]
client_ctx: self.client_ctx.clone(),
#[cfg(feature = "gssapi")]
#[cfg(any(feature = "gssapi", feature = "ntlm"))]
tls_endpoint_token: self.tls_endpoint_token.clone(),
has_tls: self.has_tls,
last_id: 0,
Expand Down Expand Up @@ -376,6 +376,89 @@ impl Ldap {
Ok(res)
}

#[cfg_attr(docsrs, doc(cfg(feature = "ntlm")))]
#[cfg(feature = "ntlm")]
/// Do an SASL GSS-SPNEGO bind with an NTLMSSP exchange on the connection. Username
/// and password must be provided, since the method is incapable of retrieving the
/// credentials associated with the login session (which would only work on Windows
/// anyway.) To specify the domain, incorporate it into the username, using the
/// `DOMAIN\user` or `user@DOMAIN` format.
///
/// __Caveat:__ the connection is not encrypted by NTLM "sealing". For encryption, use
/// TLS. Additionally, no channel binding token is sent on a TLS connection, so some
/// strictly configured servers may refuse to work. If possible, use Kerberos/GSSAPI.
pub async fn sasl_ntlm_bind(&mut self, username: &str, password: &str) -> Result<LdapResult> {
const LDAP_RESULT_SASL_BIND_IN_PROGRESS: u32 = 14;

use sspi::{
builders::AcquireCredentialsHandleResult, AuthIdentity, AuthIdentityBuffers,
ClientRequestFlags, CredentialUse, DataRepresentation, Ntlm, SecurityBuffer,
SecurityBufferType, SecurityStatus, Sspi, SspiImpl, Username,
};

fn step(
ntlm: &mut Ntlm,
acq_creds: &mut AcquireCredentialsHandleResult<Option<AuthIdentityBuffers>>,
input: &[u8],
) -> Result<Vec<u8>> {
let mut input = vec![SecurityBuffer::new(
input.to_vec(),
SecurityBufferType::Token,
)];
let mut output = vec![SecurityBuffer::new(Vec::new(), SecurityBufferType::Token)];
let mut builder = ntlm
.initialize_security_context()
.with_credentials_handle(&mut acq_creds.credentials_handle)
.with_context_requirements(ClientRequestFlags::ALLOCATE_MEMORY)
.with_target_data_representation(DataRepresentation::Native)
.with_input(&mut input)
.with_output(&mut output);
let result = ntlm
.initialize_security_context_impl(&mut builder)?
.resolve_to_result()?;
match result.status {
SecurityStatus::CompleteNeeded | SecurityStatus::CompleteAndContinue => {
ntlm.complete_auth_token(&mut output)?
}
s => s,
};
Ok(output.swap_remove(0).buffer)
}

let mut ntlm = Ntlm::new();
let identity = AuthIdentity {
username: Username::parse(username).unwrap(),
password: password.to_string().into(),
};
let mut acq_creds = ntlm
.acquire_credentials_handle()
.with_credential_use(CredentialUse::Outbound)
.with_auth_data(&identity)
.execute(&mut ntlm)?;
let req = sasl_bind_req("GSS-SPNEGO", Some(&step(&mut ntlm, &mut acq_creds, &[])?));
let (res, _, token) = self.op_call(LdapOp::Single, req).await?;
if res.rc != LDAP_RESULT_SASL_BIND_IN_PROGRESS {
return Ok(res);
}
let token = match token.0 {
Some(token) => token,
_ => return Err(LdapError::NoNtlmChallengeToken),
};
if self.has_tls {
let mut cbt = Vec::from(&b"tls-server-end-point:"[..]);
if let Some(ref token) = self.tls_endpoint_token.as_ref() {
cbt.extend(token);
// temporary private extension, will see how best to incorporate into sspi-rs
// ntlm.set_channel_bindings(&cbt);
}
}
let req = sasl_bind_req(
"GSS-SPNEGO",
Some(&step(&mut ntlm, &mut acq_creds, &token)?),
);
Ok(self.op_call(LdapOp::Single, req).await?.0)
}

/// Perform a Search with the given base DN (`base`), scope, filter, and
/// the list of attributes to be returned (`attrs`). If `attrs` is empty,
/// or if it contains a special name `*` (asterisk), return all (user) attributes.
Expand Down
13 changes: 13 additions & 0 deletions src/result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,19 @@ pub enum LdapError {
/// No token received from GSSAPI acceptor.
#[error("no token received from acceptor")]
NoGssapiToken,

#[cfg(feature = "ntlm")]
/// SSPI error in NTLM processing.
#[error("SSPI NTLM error: {source}")]
SSPIError {
#[from]
source: sspi::Error,
},

#[cfg(feature = "ntlm")]
/// No CHALLENGE token received in NTLM exchange.
#[error("no CHALLENGE token received in NTLM exchange")]
NoNtlmChallengeToken,
}

impl From<LdapError> for io::Error {
Expand Down
9 changes: 9 additions & 0 deletions src/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,15 @@ impl LdapConn {
rt.block_on(async move { ldap.sasl_gssapi_bind(server_fqdn).await })
}

#[cfg_attr(docsrs, doc(cfg(feature = "ntlm")))]
#[cfg(feature = "ntlm")]
/// See [`Ldap::sasl_ntlm_bind()`](struct.Ldap.html#method.sasl_ntlm_bind).
pub fn sasl_ntlm_bind(&mut self, username: &str, password: &str) -> Result<LdapResult> {
let rt = &mut self.rt;
let ldap = &mut self.ldap;
rt.block_on(async move { ldap.sasl_ntlm_bind(username, password).await })
}

/// See [`Ldap::search()`](struct.Ldap.html#method.search).
pub fn search<'a, S: AsRef<str> + Send + Sync + 'a, A: AsRef<[S]> + Send + Sync + 'a>(
&mut self,
Expand Down

0 comments on commit 4ba1239

Please sign in to comment.