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
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-style 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. It is exported so that layered schemes (e.g. ARC) can verify their DKIM-shaped signature — an ARC-Message-Signature is structurally a DKIM-Signature — using exactly the same canonicalization and crypto path.