Skip to content

feat: support CRAM-MD5 authentication (RFC 2195)#11

Merged
iamd3vil merged 1 commit into
mainfrom
feat/cram-md5-auth
Jul 16, 2026
Merged

feat: support CRAM-MD5 authentication (RFC 2195)#11
iamd3vil merged 1 commit into
mainfrom
feat/cram-md5-auth

Conversation

@iamd3vil

Copy link
Copy Markdown
Owner

Summary

Adds AUTH CRAM-MD5 support alongside the existing PLAIN and LOGIN mechanisms. No config changes needed — the existing [[server.auth]] username/password credentials serve all three mechanisms, since CRAM-MD5 verification just recomputes the HMAC from the stored plaintext password. The mechanism is negotiated per-connection by the client, so there is no per-credential auth_type knob.

smtp crate

  • New AuthCramMd5 / AuthCramMd5Response commands and AuthenticatingCramMd5(challenge) session state.
  • Issues an RFC 2195 challenge (<counter.nanos@hostname>, using the configurable hostname from feat: make inbound SMTP greeting/EHLO hostname configurable #9). The atomic counter + timestamp guarantees per-session uniqueness — which is what blocks digest replay — without adding a rand dependency.
  • New on_auth_cram_md5(username, challenge, digest) trait callback plus a supports_cram_md5() capability gate (both defaulted, non-breaking): implementations that don't opt in neither advertise CRAM-MD5 in EHLO nor accept it (504 instead).

hedwig

  • on_auth_cram_md5 recomputes HMAC-MD5(password, challenge) and compares digests in constant time. Lowercase hex per the RFC; uppercase tolerated. hmac and md-5 added as direct deps — both were already in the tree transitively.

AUTH exchange hardening (all mechanisms)

  • Cancelling an in-progress exchange with * (RFC 4954 §4) now gets 501 and returns the session to Greeted instead of tearing down the connection.
  • Malformed base64 / malformed response data likewise gets 501 instead of a connection-killing internal error. Both flaws pre-existed in the PLAIN/LOGIN paths and are fixed uniformly.

Note

CRAM-MD5 is a legacy-client compatibility feature, not a security upgrade: it avoids sending the password in cleartext, but MD5-based challenge-response is obsolete and PLAIN over TLS remains the stronger option.

Testing

  • RFC 2195 known-answer vector for the digest computation.
  • Protocol-flow tests over an in-memory duplex stream: fresh challenge per attempt, retry after 535, cancellation, malformed responses, 504 when the callback doesn't support the mechanism, recovery after malformed AUTH PLAIN.
  • Rejection cases: wrong password, replayed challenge, unknown user, uppercase hex accepted.
  • Verified end-to-end against a live server with Python smtplib forcing CRAM-MD5: 235 on correct password, 535 on wrong, mail accepted after auth.
  • 170 tests passing; clippy and fmt clean. Reviewed with codex (3 findings, all fixed — final pass clean).

Advertise and accept AUTH CRAM-MD5 alongside PLAIN and LOGIN. The
existing [[server.auth]] username/password credentials serve all three
mechanisms, so no config changes are needed.

- smtp: new AuthenticatingCramMd5 session state, challenge generation
  (<counter.nanos@hostname>, unique per session to prevent digest
  replay), and an on_auth_cram_md5 callback with a supports_cram_md5
  capability gate so implementations that don't support the mechanism
  neither advertise it nor accept it (504 instead).
- hedwig: verify the client digest by recomputing HMAC-MD5(password,
  challenge) from the stored password, compared in constant time.
  hmac and md-5 were already in the tree as transitive deps.
- Harden the AUTH exchange for all mechanisms: cancellation with '*'
  (RFC 4954) and malformed base64/response data now get a 501 and
  return the session to Greeted instead of tearing down the connection.

Verified end-to-end with Python smtplib forcing CRAM-MD5 against a
live server: 235 on correct password, 535 on wrong, mail accepted
after auth.
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying hedwig with  Cloudflare Pages  Cloudflare Pages

Latest commit: 328f02e
Status: ✅  Deploy successful!
Preview URL: https://e6f58e2a.hedwig-1mq.pages.dev
Branch Preview URL: https://feat-cram-md5-auth.hedwig-1mq.pages.dev

View logs

@iamd3vil
iamd3vil merged commit e81d460 into main Jul 16, 2026
2 checks passed
@iamd3vil
iamd3vil deleted the feat/cram-md5-auth branch July 16, 2026 06:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant