Skip to main content

Crate mailrs_srs

Crate mailrs_srs 

Source
Expand description

§mailrs-srs

Crates.io docs.rs License

Sender Rewriting Scheme (SRS) primitive for SPF-aware mail forwarding.

When a forwarder relays mail with the original envelope-From intact, the receiver’s SPF check sees the forwarder’s IP against the original sender’s domain — and rejects. SRS rewrites the envelope-From into a form that:

  1. Identifies the forwarder as the responsible sender (passes SPF)
  2. Carries the original sender encoded into the local-part, so bounces can be reverse-mapped back to the original

mailrs-srs provides both directions: forward rewrite() for the outgoing relay, reverse reverse() for incoming bounces with full HMAC verification + timestamp-window check.

§Quickstart

use mailrs_srs::{rewrite, reverse, DEFAULT_TIMESTAMP_WINDOW_DAYS};

// Forward: rewrite envelope-From when relaying a message.
let original = "alice@example.com";
let forwarder_domain = "mx.golia.jp";
let secret = "shared-secret-32-bytes-or-whatever";

let rewritten = rewrite(original, forwarder_domain, secret);
//  →  "SRS0=ab12cd34=072=example.com=alice@mx.golia.jp"

// Reverse: when a bounce comes back to the SRS-rewritten address,
// verify the HMAC + parse the original sender.
let bounce_target = &rewritten;
let recovered = reverse(bounce_target, secret, DEFAULT_TIMESTAMP_WINDOW_DAYS);
assert_eq!(recovered.as_deref(), Some("alice@example.com"));

// Wrong secret: rejects.
assert!(reverse(bounce_target, "wrong-secret", 14).is_none());
// Tampered hash: rejects.
let tampered = bounce_target.replace("SRS0=", "SRS1=");
assert!(reverse(&tampered, secret, 14).is_none());

§Wire format

SRS0=<hash>=<tt>=<original-domain>=<local-part>@<local-domain>
        │     │           │             │            │
        │     │           │             │            └─ forwarder's own domain
        │     │           │             └─ original sender's local-part
        │     │           └─ original sender's domain
        │     └─ timestamp: days_since_epoch mod 1024, 3-digit zero-padded
        └─ HMAC-SHA256 truncated to 8 hex chars (32 bits)

The 32-bit truncated HMAC is enough to prevent online guessing (one attempt per network round trip) given a per-day timestamp. The timestamp window defaults to 14 days; bounces older than that fail verification, which keeps stale bounce traffic from chewing through attacker probe attempts.

SRS1 (forwarding-chain) is not implemented in 1.0; it’s only needed when a forwarder receives mail that was already SRS-rewritten by another forwarder (multi-hop forwarding). Most deployments don’t see this; if you need it, file an issue.

§Constant-time HMAC comparison

reverse() uses constant-time byte comparison on the HMAC bytes so an attacker who can call your reverse-lookup endpoint in a loop can’t recover the secret by timing-side-channel. (The HMAC value itself is not secret — the key used to compute it is. The constant-time check prevents byte-by-byte secret recovery.)

§What this crate is not

  • Not a full SMTP relay implementation. Plug rewrite() / reverse() into your existing relay’s envelope-handling.
  • Not an SPF verifier. Use mail-auth (or whatever crate you have) for the actual SPF lookup; SRS only solves the “forwarder’s IP fails the original sender’s SPF” problem.
  • Not a DKIM rewriter. DKIM signatures cover the message body + selected headers, not the envelope, so DKIM forwarding is unrelated to SRS.

§Performance

Measured (criterion, M-series Mac, release, 100-sample median):

OperationMedian
rewrite (ASCII sender)~270 ns
reverse (success path, in-window)~290 ns
reverse (wrong-secret rejection)~280 ns (constant-time)

Run: cargo bench -p mailrs-srs --bench srs. Reproduce numbers in BUDGETS.md.

§License

Apache-2.0 OR MIT. Internal layout: rewrite (forward) + reverse (parse + verify HMAC + return original) form the round-trip pair. Both share the same HMAC-SHA256 construction over (tt, original_domain, local_part) truncated to the first 4 bytes (8 hex chars) for compact wire form.

Constants§

DEFAULT_TIMESTAMP_WINDOW_DAYS
SRS timestamp window: rewrites older than this number of days fail verification. RFC default is 21 days; we use 14 as a tighter default (most legitimate bounces come back within hours).

Functions§

reverse
Parse an SRS-rewritten address back to the original sender, verifying the HMAC and timestamp window.
rewrite
Forward-rewrite an envelope sender for SPF-aware forwarding.