Documentation
¶
Overview ¶
Package dkim signs and verifies email messages with DomainKeys Identified Mail (DKIM, RFC 6376).
DKIM lets a domain take responsibility for a message by attaching a cryptographic signature over selected header fields and the body. A verifier fetches the signer's public key from DNS (at <selector>._domainkey.<domain>) and confirms the message was not altered in transit.
Signing and verification both operate on the raw RFC 5322 message bytes — never on a parsed or reconstructed representation — so a signature is checked against exactly what was transmitted. Both "simple" and "relaxed" header and body canonicalization are supported; signing uses rsa-sha256, and verification additionally accepts the legacy rsa-sha1 algorithm. The package depends only on the Go standard library.
Signing ¶
Sign returns a DKIM-Signature field value over a raw message; the caller prepends the header field name and a trailing CRLF:
value, err := dkim.Sign(raw, dkim.SignOptions{
Domain: "example.com",
Selector: "default",
PrivateKey: key,
})
signed := append([]byte("DKIM-Signature: "+value+"\r\n"), raw...)
GenerateKey, RecordName, RecordValue and RecordFragment produce a keypair and render the public half as the DNS TXT record verifiers look up.
Verifying ¶
Verify returns one VerifyResult per DKIM-Signature header, in header order. A nil resolver uses the system DNS resolver:
for _, r := range dkim.Verify(ctx, raw, nil) {
fmt.Println(r.Domain, r.Result)
}
Primitives ¶
The canonicalization and single-signature primitives (SplitMessage, CanonicalizeHeader, CanonicalizeBody, BuildSignedHeaders, VerifySignature, FetchKey, ParseTagList, RemoveBValue, StripWSP, HashBytes) are exported so that layered schemes such as ARC — whose ARC-Message-Signature is structurally a DKIM-Signature — can reuse the exact same code path.
Example ¶
Example signs a message and then verifies it. It uses an in-memory DNS resolver so the round trip is self-contained. In production you generate the keypair once, sign with the private half, publish the public half as the DNS TXT record at <selector>._domainkey.<domain>, and let Verify resolve it over system DNS by passing a nil resolver.
package main
import (
"context"
"fmt"
"github.com/rest-mail/go-dkim"
)
func main() {
// Generate a keypair. Keep the private key for signing; publish the public
// half as the DNS record verifiers look up.
privPEM, pubPEM, err := dkim.GenerateKey(2048)
if err != nil {
panic(err)
}
key, err := dkim.ParsePrivateKey(privPEM)
if err != nil {
panic(err)
}
raw := []byte("From: alice@example.com\r\n" +
"To: bob@example.net\r\n" +
"Subject: hello\r\n" +
"Date: Thu, 23 Jul 2026 10:00:00 +0000\r\n" +
"Message-ID: <1@example.com>\r\n" +
"\r\n" +
"Hello, world!\r\n")
// Sign returns the DKIM-Signature field value; prepend the header yourself.
value, err := dkim.Sign(raw, dkim.SignOptions{
Domain: "example.com",
Selector: "default",
PrivateKey: key,
})
if err != nil {
panic(err)
}
signed := append([]byte("DKIM-Signature: "+value+"\r\n"), raw...)
// Serve the public key we just generated from memory, so the example needs
// no real DNS. In production, pass nil to use the system resolver.
txt, err := dkim.RecordValue(pubPEM)
if err != nil {
panic(err)
}
resolver := func(_ context.Context, _ string) ([]string, error) {
return []string{txt}, nil
}
results := dkim.Verify(context.Background(), signed, resolver)
r := results[0]
fmt.Printf("d=%s s=%s -> %s\n", r.Domain, r.Selector, r.Result)
}
Output: d=example.com s=default -> pass
Index ¶
- Constants
- func BuildSignedHeaders(hTag string, allHeaders []Header, sig Header, canon string) string
- func CanonicalizeBody(body, canon string) string
- func CanonicalizeHeader(h Header, canon string) string
- func GenerateKey(bits int) (privatePEM, publicPEM string, err error)
- func HashBytes(h crypto.Hash, data []byte) []byte
- func ParsePrivateKey(pemStr string) (*rsa.PrivateKey, error)
- func ParseTagList(s string) map[string]string
- func ParseTagListStrict(s string) (map[string]string, error)
- func RecordFragment(name, value string) string
- func RecordName(selector, domain string) string
- func RecordValue(publicKeyPEM string) (string, error)
- func RemoveBValue(field string) string
- func Sign(rawMessage []byte, opt SignOptions) (string, error)
- func StripWSP(s string) string
- type Header
- type KeyFlags
- type SignOptions
- type TXTResolver
- type VerifyResult
- func Verify(ctx context.Context, rawMessage []byte, resolver TXTResolver) []VerifyResult
- func VerifySignature(ctx context.Context, sig Header, allHeaders []Header, body string, ...) VerifyResult
- func VerifySignatureBare(ctx context.Context, sig Header, allHeaders []Header, body string, ...) VerifyResult
Examples ¶
Constants ¶
const ( ResultPass = "pass" ResultFail = "fail" ResultNeutral = "neutral" ResultNone = "none" ResultTempError = "temperror" ResultPermError = "permerror" )
Verification result strings, mirroring RFC 8601 dkim= values.
const DefaultSelector = "default"
DefaultSelector is the selector RecordName and RecordFragment fall back to when none is given: "default", i.e. default._domainkey.<domain>.
Variables ¶
This section is empty.
Functions ¶
func BuildSignedHeaders ¶
BuildSignedHeaders assembles the canonicalized header block that a signature's b= tag signs: each header named in hTag (a colon-separated list, matched bottom-up per RFC 6376 §5.4.2), followed by the signature header (sig) itself with its b= value emptied and NO trailing CRLF.
It is exported so a signer or a layered scheme (ARC's ARC-Message-Signature) can produce the exact bytes VerifySignature will hash.
func CanonicalizeBody ¶
CanonicalizeBody applies simple or relaxed body canonicalization (RFC 6376 §3.4.3 / §3.4.4). Input is a CRLF-normalized body (as returned by SplitMessage). The result always ends in exactly one CRLF, except that a relaxed canonicalization of an empty body is the empty string.
func CanonicalizeHeader ¶
CanonicalizeHeader canonicalizes a single header field per RFC 6376 §3.4, using either "relaxed" or "simple" canonicalization. The returned string has NO trailing CRLF — the caller appends one between signed headers (and none after the trailing signature header being verified).
func GenerateKey ¶
GenerateKey creates an RSA DKIM keypair of the given size in bits and returns both halves PEM-encoded. The private PEM is the signing key (pass it to ParsePrivateKey); the public PEM feeds RecordValue to build the DNS record. Use at least 2048 bits for production keys.
func HashBytes ¶
HashBytes hashes data with the given hash (crypto.SHA256 or crypto.SHA1, the two algorithms RFC 6376 defines) and returns the digest.
func ParsePrivateKey ¶
func ParsePrivateKey(pemStr string) (*rsa.PrivateKey, error)
ParsePrivateKey parses a PEM-encoded RSA private key in PKCS#1 or PKCS#8 form.
func ParseTagList ¶
ParseTagList parses a DKIM tag=value list ("k=v; k2=v2") into a map. Keys and the ends of values are trimmed; internal FWS in values is preserved (callers strip it via StripWSP where it is insignificant, e.g. b=, bh=, p=). It parses DKIM-Signature, DKIM key records, and ARC header (i=, cv=, …) tag lists alike.
func ParseTagListStrict ¶ added in v0.1.3
ParseTagListStrict parses a DKIM tag=value list like ParseTagList but enforces the RFC 6376 §3.2 well-formedness rules the verification paths require:
- "Tags with duplicate names MUST NOT occur within a single tag-list; if a tag name does occur more than once, the entire tag-list is invalid." Silently taking one of the values (as a last-wins parse does) lets two verifiers reach different verdicts about the same message, so a repeated tag name is an error.
- A non-empty segment that lacks "=" is a malformed tag-spec and is an error (§6.1.1 cautions against being liberal in what is accepted). An empty segment — e.g. from the optional trailing ";" the ABNF allows, or surrounding folding whitespace — is ignored, not rejected.
- A segment with an empty tag name ("=value") is malformed (tag-name = ALPHA *ALNUMPUNC) and is an error.
On any violation it returns a nil map and an error, so a caller verifying a DKIM-Signature or a DNS key record can PERMFAIL (or skip the record) rather than resolve a malformed list to an arbitrary value.
func RecordFragment ¶
RecordFragment renders a dnsmasq txt-record line for a DKIM record, splitting the value into <=255-char strings (the DNS TXT per-string limit) so that 2048-bit records — whose p= value exceeds 255 chars — stay valid:
txt-record=default._domainkey.d,"chunk1","chunk2"
func RecordName ¶
RecordName returns the DKIM record name <selector>._domainkey.<domain>.
func RecordValue ¶
RecordValue renders the DKIM DNS TXT value (v=DKIM1; k=rsa; p=<base64 DER>) from a PEM-encoded RSA public key (as produced by GenerateKey).
func RemoveBValue ¶
RemoveBValue blanks the value of the b= tag in a signature field (a DKIM-Signature, ARC-Message-Signature, or ARC-Seal value) while preserving every other byte (including the "b=" itself), as required before canonicalizing the signature header for verification.
func Sign ¶
func Sign(rawMessage []byte, opt SignOptions) (string, error)
Sign computes a DKIM-Signature header field VALUE (everything after "DKIM-Signature:") over the given raw RFC 5322 message. The caller prepends it as "DKIM-Signature: " + value + "\r\n".
Signing operates on the message's ACTUAL header/body bytes and shares its canonicalization with Verify (via BuildSignedHeaders and CanonicalizeBody), so a message signed here verifies here — and, because it signs the real transmitted bytes rather than a reconstruction, at any RFC 6376 verifier. Only headers that are actually present are included in h=.
Types ¶
type Header ¶
type Header struct {
// Name is the field name, e.g. "From" (whitespace-trimmed, original case).
Name string
// Value is everything after the colon, with folding CRLFs preserved and the
// trailing CRLF stripped.
Value string
// Raw is the full field exactly as it appeared (name, colon, value, folds),
// with no trailing CRLF — used by simple header canonicalization.
Raw string
}
Header is one parsed header field of an RFC 5322 message: its name, its value (everything after the colon, folding CRLFs preserved, trailing CRLF stripped) and the full raw field. It is the unit SplitMessage produces and that CanonicalizeHeader / VerifySignature / BuildSignedHeaders consume.
func SplitMessage ¶
SplitMessage normalizes line endings to CRLF and splits a raw RFC 5322 message into ordered header fields and the body. Line-ending normalization (bare LF / lone CR → CRLF) reconstructs the canonical wire form the signer hashed, in case an intermediate stored the message with LF-only endings.
It is exported so that layered signature schemes (e.g. ARC) can parse a raw message into the same Header slice / body that Sign and Verify operate on.
type KeyFlags ¶ added in v0.2.0
type KeyFlags struct {
// NoSubdomain reflects the key record's t=s flag (RFC 6376 §3.6.1): when set,
// any signature carrying an i= (AUID) tag MUST have its domain equal to d=
// exactly — subdomain AUIDs are not permitted. Absent the flag (the default),
// a subdomain i= is allowed.
NoSubdomain bool
// NotForEmail reflects the key record's s= service-type tag (RFC 6376
// §3.6.1): s= is a colon-separated list of the service types the key may be
// used for, defaulting to "*" (all). When set — meaning s= was present and
// listed neither "email" nor the wildcard "*" — the key MUST NOT be used to
// verify an email signature. Absent the tag (default "*"), or a list that
// includes "email" or "*", leaves it unset and the key usable for email.
NotForEmail bool
}
KeyFlags carries the policy-bearing tags of the DKIM key record the verifier selected — the flags it must apply against the signature after the key itself resolves, as distinct from the record fields (p=, k=, h=) FetchKey consumes to produce the key. It is returned by FetchKey so a caller (VerifySignature, or a layered scheme reusing the same key path) can enforce them.
func FetchKey ¶
func FetchKey(ctx context.Context, selector, domain, hashAlg string, resolver TXTResolver) (*rsa.PublicKey, KeyFlags, string)
FetchKey resolves and parses a signer's RSA public key from its DKIM key record at <selector>._domainkey.<domain>. hashAlg is the hash half of the verifying signature's a= tag ("sha256" / "sha1"); a key record whose h= tag is present but does not list hashAlg is ignored (RFC 6376 §3.6.1 / §6.1.2). On success it returns (key, flags, "") where flags carries the selected key record's policy tags (the t= flags, §3.6.1) for the caller to enforce against the signature; on failure it returns (nil, KeyFlags{}, result) where result is ResultTempError (transient DNS failure) or ResultPermError (missing, revoked, malformed, or hash-algorithm-disallowed key).
type SignOptions ¶
type SignOptions struct {
Domain string // d= signing domain (required)
Selector string // s= selector (required)
PrivateKey *rsa.PrivateKey // signing key (required)
Headers []string // headers to sign; default from:to:subject:date:message-id
HeaderCanon string // "relaxed" (default) or "simple"
BodyCanon string // "relaxed" (default) or "simple"
Time int64 // t= value; 0 omits the tag
}
SignOptions configures DKIM signing. Zero-value fields fall back to the documented defaults.
type TXTResolver ¶
TXTResolver looks up DNS TXT records for a name. It matches the signature of net.Resolver.LookupTXT so the default resolver can be used directly, and a stub can be injected in tests.
type VerifyResult ¶
type VerifyResult struct {
Domain string // d= signing domain
Selector string // s= selector
Result string // one of the Result* constants
Reason string // human-readable detail
}
VerifyResult is the outcome of verifying a single DKIM-Signature.
func Verify ¶
func Verify(ctx context.Context, rawMessage []byte, resolver TXTResolver) []VerifyResult
Verify performs RFC 6376 DKIM verification against a raw RFC 5322 message.
Verification MUST run over the exact bytes that were signed — the header and body as transmitted — so this operates on the raw message, never on a parsed or reconstructed representation (reconstructing headers/body from structured fields would not reproduce the signer's canonicalization for anything but the simplest messages).
It returns one VerifyResult per DKIM-Signature header found, in header order. An empty slice means the message carried no DKIM-Signature. A nil resolver uses the system DNS resolver.
func VerifySignature ¶
func VerifySignature(ctx context.Context, sig Header, allHeaders []Header, body string, resolver TXTResolver) VerifyResult
VerifySignature verifies a single DKIM-Signature header (sig) against the message it covers: allHeaders is the full ordered header block the signature's h= tag selects from, and body is the CRLF-normalized body (both as returned by SplitMessage). It performs body-hash, header-hash and public key checks and returns a VerifyResult.
It is the per-signature verification primitive underneath Verify and applies the FULL RFC 6376 DKIM-Signature policy: v= is required and must be 1, From must be signed, an i= (AUID) must be aligned with d=, timing (t=/x=) is enforced, and the key record's t=s / s= policy flags are applied.
A layered scheme (e.g. ARC) whose signature is structurally a DKIM-Signature but versionless and governed by its own rules should NOT call this — it would inherit DKIM policy that does not apply. Call VerifySignatureBare instead: the same canonicalization and crypto path with none of the DKIM policy, and apply the scheme's own policy on top.
func VerifySignatureBare ¶ added in v0.2.1
func VerifySignatureBare(ctx context.Context, sig Header, allHeaders []Header, body string, resolver TXTResolver) VerifyResult
VerifySignatureBare verifies ONLY the cryptographic mechanism of a DKIM-shaped signature and applies NONE of the RFC 6376 DKIM-Signature policy. It selects the h= headers, canonicalizes header and body per c= (honoring l=), hashes per a=, resolves the signer's public key from d=/s= via DNS, and checks the b= signature. It does NOT require a v= version tag, does NOT require From to be signed, does NOT check i= (AUID) alignment to d=, does NOT apply signature timing (t=/x=), and does NOT enforce the key record's t=s / s= policy flags.
It exists so a layered scheme can verify a signature that is structurally a DKIM-Signature but governed by its OWN policy, then apply that policy itself. The canonical case is ARC: an ARC-Message-Signature is versionless by design (RFC 8617 §4.1.2) and carries no author-alignment semantics, so verifying it under full DKIM policy is wrong. An ARC verifier calls VerifySignatureBare to check the AMS mechanism and then enforces RFC 8617 itself. Selecting the right key (the key record's own v=DKIM1 / h= constraints) is part of the mechanism and still applies.
For standalone DKIM use Verify or VerifySignature — which is exactly this primitive plus the full RFC 6376 policy — NOT this function.