There are two "from" addresses in every email.
The envelope sender (MAIL FROM) is spoken during the SMTP conversation, used for bounces, and invisible in every mail client on earth. The From: header is the only one a human ever reads.
SPF checks the first one.
So an attacker can register a domain, publish a perfectly valid SPF record for it, send from an IP that record authorises, collect a clean spf=pass — and still put your name in the From: header. The check passed. The check was never looking at the thing the victim reads.
DKIM has the opposite shape: it signs the message itself, survives forwarding, and carries its own d= domain — which also does not have to match From:.
DMARC is the small piece of glue that makes the other two mean anything: it demands that at least one of them pass and be aligned with the visible From:.
Live, paste a mock zone and a raw message: https://dev48.infy.uk/solve/day62-spf-dmarc-checker.html
Fake the resolver first, and model both kinds of nothing
Before any protocol code, write the resolver stub — and make it distinguish NXDOMAIN from an existing name with no records of that type. SPF treats those differently, and if your stub collapses both into null you will produce the wrong result on a and mx mechanisms without ever seeing why.
check_host() walks left to right and stops at the first match
RFC 7208 is not a filter and not a scoring function. It is an ordered walk over terms where the first match wins, with qualifiers deciding what a match means:
| qualifier | result |
|---|---|
+ |
pass (default) |
- |
fail |
~ |
softfail |
? |
neutral |
The engine here implements ip4/ip6 CIDR containment for both families with no library, plus a, mx, ptr, exists, include: and redirect=.
The budget of ten, counted along the path actually taken
SPF caps DNS-querying mechanisms at ten. The subtlety that catches people is that the limit counts terms actually evaluated, not terms present in the record — and include: recursion carries its own budget consumption up the chain.
A record that trips the limit is a permerror, and a permerror can never satisfy DMARC. A chain of vendor include: statements that each looked reasonable in isolation is one of the most common ways a domain quietly loses authentication.
include: is a recursive call with a translation table
include: is not a textual paste. It is a recursive check_host() whose result is translated: a pass inside the include becomes a match for the outer term, and a fail becomes "keep walking". Implementing it as a string splice produces subtly wrong answers that only show up on failure paths.
Macros: SPF has a string language almost nobody knows
%{i}, %{s}, %{d}, with reversal and truncation modifiers. Rarely used, fully specified, and implemented here — because "rarely used" is exactly where the surprises live.
DKIM: the bytes that were actually hashed
The page implements simple and relaxed canonicalisation for both headers and body, then hashes with WebCrypto SHA-256. This is where DKIM debugging actually happens: one stray trailing space changes the body hash, and the page shows you the exact byte sequence that was fed to the hash so you can see it break in front of you.
The signed header set, the b= value emptied before hashing itself, the bh= comparison — all of it visible rather than asserted.
DMARC: organizational domains and alignment
Identifier alignment is computed against the organizational domain, not the full hostname, in relaxed mode. Then p, sp and pct decide the disposition.
Two results the page makes concrete:
- Forwarding breaks SPF but not DKIM. The relay's IP is not in your record, so SPF fails — but the signature still verifies, so DMARC can still pass on the DKIM leg.
- SPF alone stops nothing a human can see, because of the two-from-addresses problem above.
Everything is verified against golden vectors, a second independent evaluator, and a full cross-product of configurations. Vanilla JS, no network, nothing leaves the page.
Top comments (0)