Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "openldap"
version = "1.2.2"
authors = ["Josh Leverette <coder543@gmail.com>", "Ross Delinger <rossdylan@csh.rit.edu>", "Stephen Holsapple <sholsapp@gmail.com>", "Yong Wen Chua <lawliet89@users.noreply.github.com>"]
authors = ["Josh Leverette <coder543@gmail.com>", "Ross Delinger <rossdylan@csh.rit.edu>", "Stephen Holsapple <sholsapp@gmail.com>", "Yong Wen Chua <lawliet89@users.noreply.github.com>", "Mathias Myrland <jedimemo@gmail.com>"]
license = "MIT"
readme = "README.md"
repository = "https://github.com/coder543/rust-cldap"
Expand All @@ -11,3 +11,8 @@ description = "Straightforward Rust bindings to the C openldap library. This is

[dependencies]
libc = "0.2.10"

[workspace]
members = [
"examples/simple_bind_authentication"
]
33 changes: 33 additions & 0 deletions examples/simple_bind_authentication/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Simple Bind with start_tls

This example shows how to use simple_bind in combination with start_tls
and a manager account to do a typical user authentication lookup.

Start TLS is the recommended way to do secure LDAP; ldaps:// on port 636 is deprecated.

## Running

Start the example docker using the start_example_server.sh script from
the examples directory. Then, from the simple_bind directory, do

```shell script
cargo run -- -u fry -p fry
```

## Steps that are being performed

The first step is to set up the LDAPRust instance, and perform start_tls on it.
This ensures that our communication is encrypted. Note that we are not verifying
the server certificate in this example; this is something you should do in production.

The next step is to simple_bind using our manger accounts DN and password. This
will allow us to perform an ldap_search later on.

Now, we take the incoming user name string, and perform an ldap_search for it.
Note how we are matching either email or username. Our search yields the DN
for the provided credentials.

The last step is to attempt a simple bind with the discovered DN and provided
user password. If all goes well, we are authenticated, otherwise, something
went wrong.

132 changes: 132 additions & 0 deletions examples/simple_bind_authentication/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
#[macro_use]
extern crate clap;

extern crate openldap;

use openldap::errors::*;
use openldap::*;
use std::ptr;

#[derive(Clap)]
#[clap(
name = "LDAP simple_bind_authentication with start_tls authentication example",
author = "Mathias Myrland <jedimemo@gmail.com>",
version = "0.1.0"
)]
struct AuthOpts {
#[clap(short = "u")]
user: String,

#[clap(short = "p")]
password: String,
}

fn ldap_with_start_tls(ldap_uri: &str) -> Result<RustLDAP, LDAPError> {
let ldap = RustLDAP::new(ldap_uri).unwrap();

ldap.set_option(
codes::options::LDAP_OPT_PROTOCOL_VERSION,
&codes::versions::LDAP_VERSION3,
);

// WARNING: Normally you would want to verify the server certificate to avoid
// man in the middle attacks, but for this testing scenario we're using a
// generated self signed certificate from the docker container.
//
// To set up certificate validation, use the LDAP_OPT_X_TLS_CACERT* options
ldap.set_option(
codes::options::LDAP_OPT_X_TLS_REQUIRE_CERT,
&codes::options::LDAP_OPT_X_TLS_NEVER,
);

ldap.set_option(openldap::codes::options::LDAP_OPT_X_TLS_NEWCTX, &0);

ldap.start_tls(None, None)?;

Ok(ldap)
}

fn do_simple_bind(
ldap: &RustLDAP,
ldap_manager_user: &str,
ldap_manager_pass: &str,
) -> Result<(), LDAPError> {
let bind_result = ldap.simple_bind(ldap_manager_user, ldap_manager_pass)?;

match bind_result {
v if v == openldap::codes::results::LDAP_SUCCESS => Ok(()),
_ => Err(LDAPError::from(String::from(
"Authentication with simple bind failed",
))),
}
}

fn ldap_dn_lookup(ldap: &RustLDAP, who: &str) -> Result<String, LDAPError> {
// Show all DNs matching the description "Human"
// ldap_search is a powerful query language, look at
// https://confluence.atlassian.com/kb/how-to-write-ldap-search-filters-792496933.html
// for an overview
//
// This particular filter allows the user to sign in with either
// uid or email
let filter = format!("(|(uid={})(mail={}))", who, who);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is vulnerable to LDAP injection. It's actually a problem of the library since it does not provide the proper tools for escaping, so I opened #9 to track this issue.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t think the C LDAP library provided this functionality, and since this is a thin wrapper around it it was never present. The C library likely assumed that you escaped your queries yourself, as I did when I was using it for the project the fork was made for.

That being said, I do not think it’s a bad idea to add that ability if there is a standardized way to perform it and it is not required to be done if you can prove that your queries are otherwise ‘safe’.


match ldap.ldap_search(
"ou=people,dc=planetexpress,dc=com",
codes::scopes::LDAP_SCOPE_SUBTREE,
Some(filter.as_str()),
Some(vec!["dn"]),
true,
None,
None,
ptr::null_mut(),
-1,
) {
Ok(search_results) => {
for result_map in search_results {
for result_tuple in result_map {
println!("Found result map with key {}", result_tuple.0);
for result_data in result_tuple.1 {
println!("\t {}", result_data);
return Ok(result_data);
}
}
}

Err(LDAPError::from(String::from(
"Authentication with simple bind failed",
)))
}
_ => Err(LDAPError::from(String::from(
"Authentication with simple bind failed",
))),
}
}

fn main() {
let options = AuthOpts::parse();
let user_to_authenticate = options.user;
let pwd_to_authenticate = options.password;

let ldap_uri = "ldap://localhost:389";
let ldap_manager_dn = "cn=Hubert J. Farnsworth,ou=people,dc=planetexpress,dc=com";
let ldap_manager_pass = "professor";

let ldap = ldap_with_start_tls(ldap_uri).unwrap();

// Bind to the LDAP server with the manager account,
// this is done to perform a search for the DN to
// use when authenticating the user attempting to
// sign in. Obviously, the manager credentials should
// be kept secret, and not be put under version control.
// In our test scenario, the professor is the manager.
do_simple_bind(&ldap, ldap_manager_dn, ldap_manager_pass).unwrap();

if let Ok(fry_dn) = ldap_dn_lookup(&ldap, user_to_authenticate.as_str()) {
// Now, perform a bind with the DN we found matching the user attempting to sign in
// and the password provided in the authentication request
do_simple_bind(&ldap, fry_dn.as_str(), pwd_to_authenticate.as_str()).unwrap();

println!("Successfully signed in as fry");
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is vulnerable to a timing side-channel attack. An attacker that does not have a valid set of credentials should not be able to learn which user accounts exist. However, if the attacker tries to authenticate with a bunch of credentials, they may notice that certain authentication attempts take longer than others. That's because the second simple-bind is only performed if ldap_dn_lookup returns Some. Therefore, the attacker can infer that a long request duration means that the username exists, even if the password is still wrong. This allows the attacker to perform a brute-force search for valid credentials much more efficiently.

Since this is a very common problem with authentication code, the example should demonstrate how to do it more safely. The basic idea is that you do the second simple-bind even if ldap_dn_lookup does not return any results. But you set a flag to remind yourself that you have to ignore the result of that second simple-bind. Here's an example from one of my programs how that looks (although in Go, not in Rust): https://github.com/majewsky/alltag/blob/df161b55fa4c7eba0abec82d2cf0df34e49b0ad4/internal/auth/ldap.go#L96-L115

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! I'll see if I get time to update the example this weekend!

}
3 changes: 3 additions & 0 deletions examples/start_example_server.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#! /usr/bin/env bash

docker run -p 389:389 -p 636:636 rroemhild/test-openldap