LDAP

Crate hopf-ldap: async LDAPv3 client (RFC 4511) and LdapCredentialStore — Gumdrop ldap / LDAPRealm port. BER wire codec lives in hopf_ldap::asn1; the store implements CredentialStore without putting LDAP types into hopf-auth.

Standards

TopicReference
LDAPv3RFC 4511
Authentication methods / LDAPSRFC 4513
String representation of filtersRFC 4515
Content Synchronization (syncrepl), client sideRFC 4533
BER (definite length)ITU-T X.690 (LDAP subset)

Architecture

Mail / HTTP / FTP
        │
        ▼
hopf-auth::CredentialStore  ←── PasswordStore (demo)
        │                   ←── hopf-ldap::LdapCredentialStore
        │                   ←── hopf-auth::PamCredentialStore (feature pam)
        ▼
hopf-ldap::LdapClient  →  ProtocolHandler on Runtime
        │
        ▼
hopf-ldap::asn1 (BER)

CredentialStore stays protocol-agnostic (same as Gumdrop Realm). LDAP lives only in hopf-ldap; system PAM is optional feature pam on hopf-auth.

BER codec

Module hopf_ldap::asn1 ports Gumdrop’s definite-length BER stack: BerEncoder, BerDecoder (streaming receive / next), Asn1Element, tag helpers. Indefinite length is rejected. This is LDAP-oriented BER, not full DER/CER.

Client

LdapClient dials via Runtime::connect / TcpConnectorConfig. On ready it delivers an LdapSession:

use hopf_ldap::{LdapClient, LdapClientConfig};

let cfg = LdapClientConfig::new("ldap.example.com", 389);
LdapClient::connect(&rt, cfg, |ready| {
    if let Ok(session) = ready {
        session.bind_anonymous(|r| { let _ = r; });
    }
})?;

LDAPS / STARTTLS

LDAPS (implicit TLS from dial, port 636 typical): LdapClientConfig::with_tls / LdapStoreConfig::with_ldaps. The handler waits for security_established before delivering the session.

STARTTLS (RFC 4511 §4.14): dial plaintext, then LdapSession::start_tls (or configure LdapStoreConfig::with_starttls so the store does it before bind). Uses Endpoint::start_client_tls after a successful ExtendedResponse for OID 1.3.6.1.4.1.1466.20037. LDAPS and STARTTLS are mutually exclusive on one dial.

Referral chase

LdapCredentialStore does not chase search/bind referrals by default (enable with with_chase_referrals(true); hop cap max_referral_hops, default 5). URLs are parsed per RFC 4516 (ldap:// / ldaps://); empty DN in the URL keeps the referring operation’s base DN (RFC 4511 §4.1.10). ldaps:// referrals need a TLS connector already configured on the store.

Content synchronization (RFC 4533)

A client that keeps a local copy of a directory subtree can resume incrementally after a disconnect instead of polling full search results. LdapSession::sync is an ordinary search carrying the Sync Request Control; its events are the server's entries (each tagged with a Sync State Control), cookie updates and phase markers, delivered on the reactor in the order sent. SyncReplica applies them.

let mut replica = SyncReplica::new();          // cookie() is None: an initial sync

replica.begin_refresh();
let mut request = SyncRequest::new(SyncMode::RefreshOnly);
if let Some(c) = replica.cookie() { request = request.with_cookie(c.to_vec()); }

session.sync(
    SearchRequest::new("dc=example,dc=com", "(objectClass=*)"),
    request,
    move |event| { let _ = replica.apply(&event); },     // entries, cookies, phase markers
    move |done| { /* Ok(SyncDone): replica.finish(&done); Err(_): failed or cancelled */ },
);

LdapCredentialStore

Search-then-bind (Gumdrop LDAPRealm.passwordMatch):

  1. Service bind (bind_dn / bind_password) or anonymous
  2. Search base_dn with user_filter ({0} = RFC 4515–escaped username)
  3. Bind as discovered user DN + candidate password
CredentialStore methodLDAP store
supported_mechanismsPLAIN, LOGIN
password_matchsearch-then-bind
digest / SCRAM / CRAM / plaintext / bearerunsupported

Reactor rule: password_match blocks on a Condvar + timeout completed from LDAP callbacks. Call it only from a storage/worker pool — never on a reactor thread.

Wiring mail services

Same with_store surface as PasswordStore:

use hopf_ldap::{LdapCredentialStore, LdapStoreConfig};
use hopf_smtp::{SmtpConfig, SmtpService};

let ldap = Arc::new(LdapCredentialStore::new(
    LdapStoreConfig::new("ldap.example.com", "dc=example,dc=com", Arc::clone(&rt))
        .with_bind("cn=admin,dc=example,dc=com", "secret")
        .with_user_filter("(uid={0})"),
));
let config = SmtpConfig::new(listen, "mail.example.com")
    .with_store(ldap);

Example binary: examples/smtp-ldap (cargo run -p smtp-ldap -- …). POP3/IMAP use the same Arc<dyn CredentialStore>.

Limitations