rustlavel-ldap: authenticate a person against a directory.
LDAP is how most organisations answer "who is this and what is their
password", and an application that needs that answer should not need to
learn ASN.1 to get it. So the package has two faces: [directory], which is
one call, and [protocol] and [ber] underneath it for anything that call
does not cover.
use rustlavel_ldap::prelude::*;
let directory = Directory::new("ldaps://dc.example.test")?
.service_account("cn=svc,dc=example,dc=test", &service_password)
.base_dn("ou=people,dc=example,dc=test")
.username_attribute("uid")
.filter(Filter::equals("objectClass", "inetOrgPerson"))
.attributes(["cn", "mail"]);
let user = directory.authenticate(&username, &password).await?;
println!("{} is {}", user.dn(), user.value("cn").unwrap_or("unnamed"));
The protocol is written here
LDAP v3 (RFC 4511) is ASN.1 BER over TCP. BER is a serialisation format
rather than cryptography, so rule one applies and it is written in [ber]:
a small, strict subset that refuses everything RFC 4511 says a directory may
not send. TLS is the exception, and comes from rustls with the aws_lc_rs
provider and prefer-post-quantum, like every other TLS in this framework.
Three things this package is firm about
A simple bind sends the password in the clear. That is what a simple
bind is — RFC 4511 §4.2 puts the password in an octet string with nothing
around it. So one is refused over an unencrypted connection unless the
caller has said, in as many words, that it is acceptable
([LdapConfig::allow_plaintext_password]). This is the most common LDAP
deployment mistake there is, and it is invisible from the outside because
everything works.
An empty password must not authenticate. LDAP treats a simple bind with
a zero-length password as an anonymous bind, which succeeds — so a login
form with a blank password field would report a successful login for any
username that exists. This has been a real vulnerability in real products
more than once. It is refused in [Operation::simple_bind], where the bytes
are built, and again in [Directory::authenticate] before any I/O happens.
A username is a value, not text. The classic LDAP injection is a filter
template with a name pasted into it: a username of * turns a lookup into a
wildcard that matches every account. [Filter] is a type, and
[Filter::encode] writes each value as a length-delimited BER octet string
where * and ) have no syntax at all. [escape_filter_value] exists for
the cases that genuinely need the RFC 4515 string form.