Auth

Crate hopf-auth: TrustPolicy, credential stores, and SASL mechanisms (Gumdrop parity except GSSAPI, which is planned). Independent of hopf-core (own VERSION). HTTP Basic / Digest / Bearer factories live in hopf-http and consume these types; FTP uses TrustPolicy directly, while POP3/IMAP/SMTP drive the full SASL mechanism set from a CredentialStore.

Standards

Mechanism / topic Reference
PLAIN RFC 4616
LOGIN Legacy draft
CRAM-MD5 RFC 2195
DIGEST-MD5 RFC 2831 (deprecated)
SCRAM-SHA-256 RFC 5802 / 7677 (PBKDF2)
SCRAM-SHA-256-PLUS RFC 5802bis / RFC 5929 tls-server-end-point channel binding — not part of Gumdrop's original seven-mechanism set; excluded from SaslMechanism::all() since no protocol crate wires real channel-binding data through yet (see Limitations)
OAUTHBEARER RFC 7628
XOAUTH2 Google's pre-standard bearer mechanism (XOAUTH2) — what Gmail and Microsoft 365 advertise on IMAP, POP3 and SMTP submission; the client answers an error challenge with the empty response the mechanism requires
EXTERNAL RFC 4422 App. A
HTTP Digest RFC 7616 helpers
Compare Constant-time via subtle

Architecture

Protocol (FTP / SMTP / HTTP auth factory)
        │
        ├── TrustPolicy::evaluate(IdentityMaterial, PeerContext)
        │         → Accept | Reject
        │
        └── SASL: create_server / create_client
                  ↔ CredentialStore (passwords, HA1, SCRAM, tokens, certs)

PasswordTrustPolicy is a small demo policy; production apps implement TrustPolicy or back SASL with a real CredentialStore.

TrustPolicy

Type Role
TrustPolicy evaluate → TrustDecision::{Accept,Reject}
IdentityMaterial UsernamePassword / Bearer / CertDn / CertSan / Opaque
PeerContext Optional peer SocketAddr
PasswordTrustPolicy In-memory username/password map
use hopf_auth::{IdentityMaterial, PeerContext, TrustDecision, TrustPolicy};

fn evaluate(&self, id: &IdentityMaterial, peer: &PeerContext) -> TrustDecision {
    // …
}

FTP stock handler and SMTP AUTH PLAIN call this layer. See FTP, SMTP.

Credential stores

Type Role
CredentialStore Trait backend (Gumdrop Realm-like)
PasswordStore In-memory SCRAM (+ optional Digest HA1), tokens, cert keys (demo; no plaintext retention)
LdapCredentialStore (hopf-ldap) Production LDAP search-then-bind (PLAIN/LOGIN); keeps LDAP out of hopf-auth
PamCredentialStore (feature pam) System PAM password check (PLAIN/LOGIN); Unix; blocking — off-reactor only
ScramCredentials StoredKey / ServerKey derivation
TokenValidation Bearer checks
CertificateIdentity Cert mapping

CredentialStore overridable methods: password_match, plaintext_password, digest_ha1, cram_md5_digest, scram_credentials, validate_bearer, authenticate_certificate, authorize_as, supported_mechanisms.

use hopf_auth::PasswordStore;

let store = PasswordStore::new()
    .with_user("alice", "secret")
    .with_token("tok", "alice")
    .with_certificate("cn=alice", "alice")
    .shared();

SCRAM iterations default 4096 (ScramCredentials::derive(password, salt, iterations)).

SASL mechanisms

pub enum SaslMechanism {
    Plain,
    Login,
    CramMd5,
    DigestMd5,
    ScramSha256,
    OauthBearer,
    External,
}

Helpers on the enum: name, from_name, is_challenge_response, requires_tls, all.

Type Role
SaslServer / SaslClient Step traits
SaslServerStep Challenge / Complete{username,final_message} / Failure
SaslClientStep Response / Complete / Failure
SaslServerOptions hostname, realm, peer_certificate, channel_binding
create_server / create_client Factories
use hopf_auth::{create_server, create_client, SaslMechanism, SaslServerOptions};

let mut server = create_server(
    SaslMechanism::ScramSha256,
    store,
    SaslServerOptions {
        hostname: "localhost".into(),
        realm: "hopf".into(),
        peer_certificate: None,
        channel_binding: None,
    },
);
let mut client = create_client(SaslMechanism::ScramSha256, "alice", "secret", "localhost", None);

// Alternate challenge/response via server.step / client.evaluate

Symmetric client + server exchanges for all seven Gumdrop-parity mechanisms, plus SCRAM-SHA-256-PLUS. requires_tls guides operators (e.g. PLAIN should be under TLS).

SCRAM channel binding

ScramSha256Client::new_plus(username, password, channel_binding) / ScramSha256Server::with_channel_binding(channel_binding) add real tls-server-end-point (RFC 5929 section 4) channel binding: the caller supplies this connection's channel-binding bytes (a hash of the server's TLS certificate — hopf-auth has no TLS awareness of its own, the same pattern ExternalServer::with_peer_certificate already uses). The server independently recomputes the expected c= value from the GS2 header the client actually sent plus its own channel-binding data and rejects a mismatch — the property that defeats a MITM relaying the SASL exchange across two different TLS connections. Not implemented: cross-mechanism downgrade detection (noticing a MITM stripped PLUS from the advertised mechanism list) — that needs the session/capability-advertisement layer, not this mechanism module.

HTTP Digest helpers

Module http_digest: challenge_header, new_nonce, verify_authorization, client_authorization. verify_authorization takes an expected_nonce: Option<&str> — real replay protection requires the caller to track issued nonces and pass the specific one being redeemed (single-use, with expiry), which hopf-http's DigestAuthFactory/DigestAuthHandler now does: each challenge nonce is tracked in a map, consumed (removed) on first use, and pruned after DigestAuthConfig::nonce_ttl (default 5 minutes, overridable via with_nonce_ttl) if never used — so a captured, cryptographically-valid Authorization: Digest header can't be replayed, even verbatim.

HTTP Basic / Digest / Bearer request wrappers are in hopf-http (BasicAuthFactory, DigestAuthFactory, BearerAuthFactory) and call into trust / digest helpers — see HTTP server.

OAuth 2.0 token introspection

Module oauth_introspection implements RFC 7662 for real, replacing the local static token map PasswordStore::validate_bearer uses by default:

Configuration

SaslServerOptions

Property Type Default Notes
hostname string "localhost" SCRAM / Digest host
realm string "hopf" Digest realm
peer_certificate optional None EXTERNAL
channel_binding optional bytes None SCRAM-SHA-256-PLUS — this connection's tls-server-end-point data

PasswordStore

Knob Default Notes
SCRAM iterations 4096 PBKDF2
Builders — with_digest_realm (before users, for DIGEST/HTTP Digest HA1), with_scram_iterations, with_user (enroll then discard password), with_scram / insert_scram, with_token, with_certificate, shared

Enrollment accepts a password once, then stores only SCRAM-SHA-256 material (and HA1 when a digest realm is set). plaintext_password always returns None — CRAM-MD5 and POP3 APOP are not supported by this store. password_match re-derives StoredKey and compares in constant time.

No Cargo features for the demo store.

PamCredentialStore

Optional feature pam on hopf-auth (and umbrella hopf feature pam). Unix only; links libpam / OpenPAM via in-tree FFI.

// Cargo.toml: hopf-auth = { version = "…", features = ["pam"] }
use hopf_auth::{PamCredentialStore, PamStoreConfig};

let store = PamCredentialStore::new(PamStoreConfig::new("login"))
    .shared();
// SmtpConfig::new(…).with_store(store)

Wiring

FTP:

let policy = PasswordTrustPolicy::default().with_user("ftp", "ftp").shared();
FtpConfig::new(addr, root, policy);

SMTP (full SASL mechanism set via CredentialStore, same as POP3/IMAP below):

SmtpConfig::new(addr, hostname)
    .with_tls(acceptor)
    .with_store(store)
    .auth_required(true);

HTTP (in hopf-http): wrap a ServerHandlerFactory with BasicAuthFactory / DigestAuthFactory / BearerAuthFactory using a TrustPolicy or realm config (BasicAuthConfig { realm }, DigestAuthConfig { realm }).

Examples

No dedicated examples/auth crate. Consumed by:

Limitations