DMARC Engine
Home/Blog/RFC 6376: the DKIM standard explained
Blog

RFC 6376: the DKIM standard explained

A line-by-line tour of the DKIM specification: how a message is signed and canonicalised, what every tag in the DKIM-Signature header means, how the public key record is published in DNS, and exactly what a verifier does on the way in.

26 March 2026 · 13 min read

RFC 6376: the DKIM standard explained

DKIM is one of those technologies that almost every domain depends on yet very few people have actually read the specification for. It sits quietly inside Authorization-Results headers, it determines whether your DMARC policy can survive a forwarded message, and it is the single biggest reason a perfectly legitimate email sometimes lands in spam. The defining document is RFC 6376, DomainKeys Identified Mail (DKIM) Signatures, published in September 2011 and elevated to an Internet Standard (STD 76) in 2013. This article walks through what that document actually says: how a message is signed, how the body and headers are canonicalised, what every tag in the DKIM-Signature header means, how the public key is published in DNS, and exactly what a verifier does when the message arrives.

This is not a generic "DKIM stops spoofing" explainer. It is a tour of the standard itself, written so that when you stare at a real signature header or a verification failure you can reason about it line by line.

What RFC 6376 actually defines (and what it does not)

RFC 6376 defines a mechanism for a domain to take responsibility for a message by attaching a cryptographic signature that a receiver can verify against a public key published in the DNS. That is the whole job. It is worth being precise about the boundaries, because a lot of confusion comes from expecting DKIM to do things the specification deliberately leaves out.

DKIM does not assert that the message is not spam. It does not assert anything about the visible From: address by itself. It does not encrypt anything; the body travels in clear text. And it makes no claim about the identity of the human who pressed send. What it asserts is narrow and durable: the domain named in the signature's d= tag is willing to be held accountable for this message, and the signed content has not been altered in transit.

The signing domain (d=) is a separate concept from the From: header domain. The two are tied together only when a higher-level protocol insists on it. That protocol is DMARC (RFC 7489), which requires the d= domain to align with the From: domain before a DKIM pass counts in its favour. RFC 6376 itself is happy for example.net to sign a message whose From: is newsletter@example.com. That separation is a feature, not a bug, and it is why your email service provider can sign on your behalf.

Three RFCs form the working set. RFC 6376 is the signing and verification core. RFC 6377 covers DKIM and mailing lists. RFC 8301 and RFC 8463 update the cryptography (deprecating SHA-1, adding Ed25519). When this article says "the spec", it means RFC 6376 as amended by those.

The signing process step by step

When a message leaves an outbound mail server that signs with DKIM, the following happens before the message is handed to the next hop.

  1. The signer chooses a signing domain (d=) and a selector (s=). Together these point at exactly one public key record in DNS.
  2. The signer selects which header fields to sign and lists them in the h= tag. The body is always covered.
  3. The signer canonicalises the chosen headers and the body using one of two algorithms for each (more on this below).
  4. The signer computes a hash of the canonicalised body and places it in the bh= (body hash) tag.
  5. The signer assembles a DKIM-Signature header containing every tag except the signature value itself, canonicalises that header too, and computes a hash over the signed headers plus this partial signature header.
  6. That hash is signed with the private key using RSA or Ed25519, and the result is base64-encoded into the b= tag.
  7. The completed DKIM-Signature header is prepended to the message.

A single message can carry multiple DKIM-Signature headers. This is common and entirely legal: the originating domain signs, then a forwarding or relaying service adds its own signature. A verifier evaluates each independently.

The critical insight is that signing happens over a transformed representation of the message, not the literal bytes. Two mail systems can render the same logical message with slightly different whitespace and line wrapping, and the signature must still verify. That is what canonicalisation exists to absorb.

Canonicalisation: the part that breaks in real life

The c= tag declares the canonicalisation algorithm in the form header/body, for example c=relaxed/relaxed. RFC 6376 defines two algorithms for each of the header and the body: simple and relaxed. They can be mixed, so simple/relaxed and relaxed/simple are valid.

Simple header canonicalisation changes nothing. The header field is hashed exactly as presented, byte for byte. This is brittle, because any intermediary that re-folds a long header or normalises a space will break the signature.

Relaxed header canonicalisation is forgiving and is what almost everyone uses. The spec says to:

  • Lower-case the header field name (values are left alone).
  • Unfold the header so continuation lines become one logical line.
  • Compress runs of whitespace inside the value to a single space.
  • Strip whitespace at the start and end of the value.
  • Remove the space after the colon following the field name.

Simple body canonicalisation removes empty lines at the very end of the body, but otherwise leaves the body untouched, including all interior whitespace.

Relaxed body canonicalisation additionally compresses runs of whitespace within a line to a single space, removes trailing whitespace on each line, and removes trailing empty lines. It is far more robust against the whitespace mangling that mail gateways inflict.

In practice you will see c=relaxed/relaxed on the overwhelming majority of legitimate mail, and you should prefer it when you control the signer. The most common cause of a body hash did not verify error is a downstream system, an anti-virus footer injector, a mailing-list [listname] subject tag, or a signature appender, modifying the body after signing. We cover the operational side of that in our guide to fixing a DKIM signature that did not verify.

There is also the optional l= (body length) tag, which tells the verifier to hash only the first N octets of the body. It was meant to let mailing lists append footers without breaking the signature. Do not use it. A length limit invites an attacker to append arbitrary content after the signed portion, which the verifier will treat as authenticated. RFC 6376 itself flags the security risk, and modern guidance is to omit l= entirely.

Anatomy of the DKIM-Signature header

Here is a real-shaped signature so we can name every part:

DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
    d=example.com; s=mail2024; t=1718925600; x=1719530400;
    h=from:to:subject:date:message-id;
    bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;
    b=dzdVyOfAKCdLXdJOc9G2q8LoXSlEniSbav+yuU4zGeeruD00lszZ
      VoG4ZHRNiYzR8N5hF0Nr5HXCpY/jdL5cVcQ==

Each tag has a defined meaning in RFC 6376:

  • v= Version. Always 1 for current DKIM. A signature without v=1 is invalid.
  • a= The signing algorithm, as keytype-hashalgo. rsa-sha256 is the standard; rsa-sha1 is deprecated by RFC 8301 and must be treated as a failure by verifiers; ed25519-sha256 is the newer compact option from RFC 8463.
  • c= Canonicalisation, header then body, as described above.
  • d= The signing domain. This, with s=, locates the public key. This is the identity DMARC checks for alignment.
  • s= The selector. A label that lets one domain publish many keys (per service, per rotation period, per provider).
  • h= The colon-separated list of header fields that were signed, in order. A field can be listed more than once to "oversign" and prevent an attacker from adding an extra copy.
  • bh= The base64 body hash, computed over the canonicalised body using the hash from a=.
  • b= The base64 signature itself, computed over the signed headers and the DKIM-Signature header with an empty b=.
  • t= Signature timestamp, seconds since the Unix epoch.
  • x= Optional expiry. After this time verifiers should treat the signature as expired regardless of the key still being published.
  • i= Optional identity of the agent or user on behalf of whom the message is signed, an @-prefixed value within or under d=. Rarely meaningful for DMARC.

The relationship between h= and b= deserves emphasis. The verifier hashes only the header fields named in h=, in the exact order given, and only the body hash is bound separately through bh=. A header not listed in h= is invisible to the signature: it can be added, changed, or removed without affecting verification. That is why best practice is to sign from at minimum, plus subject, date, to, message-id, and other meaningful fields. Notice that the From: header is the one DMARC cares about, so any sane signer always includes it in h=.

You can paste a live header into our DKIM checker to have every tag parsed and the public key fetched for you, which is faster than decoding base64 by hand.

The public key record in DNS

The verifier needs the public half of the key pair, and DKIM publishes it as a TXT record at a deterministic name:

<selector>._domainkey.<signing-domain>

So for s=mail2024; d=example.com, the verifier queries:

mail2024._domainkey.example.com

A typical key record looks like this:

mail2024._domainkey.example.com. IN TXT (
  "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQD...IDAQAB" )

The tags inside the key record are a different set from the signature header. RFC 6376 defines them as:

  • v= Key record version, DKIM1. Optional, but if present it must be the first tag.
  • k= Key type, rsa (default) or ed25519.
  • p= The base64-encoded public key. This is the only mandatory tag. A record with p= empty (p=) is a tombstone: it explicitly revokes the key, and any signature pointing at it must fail.
  • h= Optional list of acceptable hash algorithms for this key, restricting what the signer may use.
  • t= Optional flags. t=y marks the domain as being in test mode (verifiers should not treat failures as meaningful). t=s forbids subdomain i= values, requiring the signing identity to match d= exactly.
  • s= Optional service type, almost always * (any) or email.
  • n= Optional human-readable notes, ignored by software.

Two operational details trip people up. First, RSA keys are large enough that the base64 string usually exceeds the 255-character limit of a single DNS character-string, so the record is split into multiple quoted strings that the resolver concatenates. The pieces are joined with no separator. Second, 2048-bit RSA keys are now the floor; 1024-bit keys are widely considered weak, and many providers reject them. RFC 6376 set 1024 to 4096 as the supported range, but real-world expectations have moved up. If the verifier cannot find the record at all, you get the distinct error covered in DKIM key not found, which is a DNS problem, not a cryptography problem.

Selectors are the mechanism that makes key rotation painless. Because the selector is part of the DNS name, you can publish a new key under a fresh selector, start signing with it, wait for the old mail to drain, and then retire the old selector, all without a moment where verification breaks. Our note on DKIM key rotation walks through the cadence; the short version is that a key you never rotate is a key an attacker has unlimited time to attack.

Verification: what the receiver does

When a message arrives at a DKIM-aware receiver, the verifier processes each DKIM-Signature header it finds. RFC 6376 specifies the sequence:

  1. Parse the signature. Reject it immediately if v is not 1, if required tags (a, b, bh, d, h, s) are missing, if h= does not include from, or if the algorithm in a= is one the verifier refuses (such as deprecated SHA-1).
  2. Check timing. If x= is present and in the past, the signature has expired. Some verifiers also sanity-check t=.
  3. Fetch the public key. Query <s>._domainkey.<d> for the TXT record. If the lookup fails, the result is TEMPERROR (try again later) or PERMERROR (the record is broken). If p= is empty, the key is revoked and verification fails. If t=y, results are advisory.
  4. Recompute the body hash. Canonicalise the body per c=, hash it, and compare to bh=. If they differ, verification fails here, and crucially this is the body-was-modified failure, distinct from a bad key.
  5. Recompute the header hash. Take the headers named in h=, in order, canonicalise them, append the DKIM-Signature header with its b= value emptied, canonicalise that, and hash the whole.
  6. Verify the signature. Use the public key to check b= against that hash. A match means the headers and body are intact and the d= domain vouches for them.

The result is then recorded, typically in an Authentication-Results header (RFC 8601), as dkim=pass, dkim=fail, dkim=neutral, dkim=temperror, or dkim=permerror. Note the order: the body hash is checked before the cryptographic signature. This is why "body hash did not verify" and "signature did not verify" are reported as different things; the first means the content changed, the second usually means the wrong key, a corrupted b=, or a modified signed header.

A vital design point: a missing DKIM signature is not a failure. RFC 6376 says the absence of a signature carries no negative weight on its own. DKIM only ever produces a positive assertion. The job of punishing unsigned-and-unauthenticated mail belongs to DMARC, which combines DKIM and SPF results with alignment. If you have ever seen a message where SPF and DKIM both technically passed yet DMARC still failed, the cause is alignment, not verification, and we untangle exactly that in DMARC fails but SPF and DKIM pass.

How DKIM fits with SPF, DMARC and the rest

RFC 6376 is one pillar of a three-part stack, and understanding the seams matters more than understanding any single piece.

SPF authorises sending IP addresses for the envelope-from (return-path) domain. It breaks on forwarding because the forwarder's IP is not in your SPF record. DKIM authorises content through a signature that travels with the message, so it survives most forwarding. DMARC sits on top and demands that at least one of SPF or DKIM both passes and aligns with the visible From: domain. Because DKIM survives forwarding and SPF often does not, DKIM is frequently the mechanism that keeps DMARC passing on legitimately relayed mail.

This is the practical reason you cannot reach p=reject on DKIM alone or SPF alone. You want both configured and aligned so that a failure of one does not sink the message. If you are mapping out that journey, the requirements page lays out what each stage needs, and the DKIM product page explains how we manage signing, selector rotation, and key strength as a hosted service rather than a one-off setup.

ARC (RFC 8617) is worth a mention because it exists precisely to patch the one scenario where DKIM does break: a mailing list that rewrites the subject and appends a footer, invalidating the original signature. ARC lets each hop record the authentication results it saw and seal them, so a downstream receiver can choose to trust the chain. ARC does not replace DKIM; it preserves the verdict DKIM already produced.

Reading a real failure like a specification

Put the pieces together and most DKIM problems become legible. Walk the same path the verifier walks.

  • No signature at all? Your sending platform is not signing. Nothing in RFC 6376 is broken; you simply have not enabled DKIM at the source. Configure signing on the platform and publish the key.
  • dkim=permerror or key not found? A DNS problem. The TXT record at <selector>._domainkey.<domain> is missing, mis-split across strings, or contains a syntax error. Confirm the record with a DKIM checker and compare the selector in the failing header to what is published.
  • body hash did not verify? Something altered the body after signing: a footer injector, an antivirus banner, a list manager. Switch the signer to relaxed body canonicalisation if it is not already, and hunt for the gateway that edits content. Never reach for l= as a workaround.
  • signature did not verify but the body hash matched? A signed header changed in transit, the b= value was corrupted, or the key in DNS does not match the private key in use, often after a rotation that updated DNS but not the signer, or vice versa.
  • dkim=pass but DMARC still fails? Verification succeeded but the d= domain does not align with the From: domain. That is a DMARC alignment question, settled by reading your aggregate reports, not a DKIM bug.

The discipline that RFC 6376 rewards is separating the layers: DNS lookup, body hash, header signature, then alignment. Each failure mode lives at exactly one of those layers, and the verifier tells you which.

The practical takeaway

RFC 6376 is smaller and more honest than its reputation. It says: a domain can attach a cryptographic signature over a canonicalised view of a message, publish the matching public key under a selector in DNS, and let any receiver confirm the content is intact and the domain accountable. Everything else, alignment, policy, enforcement, lives in the protocols built on top.

If you are running DKIM in production, the things that actually bite are mundane and well within your control: use relaxed/relaxed canonicalisation, sign from and the other meaningful headers, publish a 2048-bit (or Ed25519) key, never use the l= length tag, and rotate selectors on a schedule so a stale key is never your weakest link.

The fastest way to see whether your own domain is signing correctly is to run a message or a published selector through our DKIM checker, and to read what your receivers are reporting through the DMARC report analyzer. If you would rather not babysit selectors, key strength, and the inevitable provider quirks yourself, that is exactly what our done-for-you DKIM service takes off your plate, with continuous monitoring that alerts you the moment a key disappears or a signature starts failing.

Share

See where your domain stands today

Run a free DMARC scan, then let us take you to enforced p=reject with no email outage.