NAME
Mail::DKIM2::Verifier - Verify the DKIM2-Signature chain on a message
SYNOPSIS
use Mail::DKIM2::Verifier;
my $verifier = Mail::DKIM2::Verifier->new->load($message);
print $verifier->result, "\n"; # pass, fail, none, permerror, temperror
print $verifier->result_detail, "\n"; # pass (i=1..3 verified)
# Streaming, with CRLF line endings:
my $v = Mail::DKIM2::Verifier->new(Resolver => $net_dns_resolver);
$v->PRINT($chunk) for @chunks;
$v->CLOSE;
# For Authentication-Results:
if (my $top = $v->top_signature) {
printf "dkim2=%s header.d=%s header.i=%d\n",
$v->result, $top->domain, $top->sequence;
}
DESCRIPTION
Verifies every DKIM2-Signature on a message, not only the outermost: the chain must be complete (i=1 to i=N with no gaps), each signature must verify over the headers that existed when it was made, consecutive hops must satisfy the chain-of-custody rules of spec-06 section 11.4, and the Message-Instance chain must undo cleanly back to the first instance, each one matching the content it describes. Past a null body Recipe (the previous body is gone) only the header history is checked. The outcome is a result and a reason, never an exception; see result below.
Extends Mail::DKIM2::HeaderParser, which provides PRINT, CLOSE, load and the tie interface. A message with no DKIM2-Signature is decided from its headers alone and its body is not kept.
This module implements draft-ietf-dkim-dkim2-spec-06; see "STATUS" in Mail::DKIM2 for what that means for the wire format and the API, and "CONVENTIONS" in Mail::DKIM2 for the option, input and error conventions every module here follows.
CONSTRUCTOR
new(%options)
All options are optional. The boolean ones and Resolver also have a snake_case method of the same name that gets or sets them after construction; PubkeyCallback has set_pubkey_callback.
- Resolver
-
A Net::DNS::Resolver (or anything with the same
queryanderrorstringmethods) for the default public-key lookup. Created on demand if not given. A milter host passes its own so its timeouts apply.If it also has Net::DNS's
bgsend,bgbusyandbgread, every key the signatures name is asked for at once, in the background, as soon as the header fields are in, and the answers are read when the signatures are checked: the lookups overlap each other, the body's arrival and the hashing. An answer that settles nothing -- none within$Mail::DKIM2::Verifier::PREFETCH_WAITseconds (5) of being sent, or SERVFAIL -- is asked again withquery, so the verdicts are those of onequeryat a time. Not with aPubkeyCallback. - PubkeyCallback
-
A code reference that replaces the DNS lookup. Called as
($signature, $index, $verifier)for each item of each signature'ss=tag; returns a Crypt::PK::RSA or Crypt::PK::Ed25519 object, undef for "no such key", or dies with a string to report a transient failure. It can fall back to$verifier->fetch_public_key($signature, $index)for keys it does not know. - SkipTimestampCheck
-
Do not fail a signature for the age of its
t=. For test fixtures. - IgnorePrefixes
-
An arrayref of header-field-name prefixes an operator's own border adds and strips, excluded from the header hash. Anything but undef or an arrayref croaks. See "Operator-local header fields" in Mail::DKIM2.
- AllowUnsignedMI
-
Permit a Message-Instance with a higher
m=than any signature covers, which spec-06 section 11 otherwise makes a permerror. For an outbound path that verifies the message it is about to sign, where the new instance legitimately exists before its signature does. Never set it on inbound mail. - NextDomainOK
-
An outbound signer's own
d=. A top signature carryingnd=is then accepted whennd=equals it (case-insensitive), and refused with "not signing: top signature nd=X names another domain" otherwise. Unset, a topnd=is the usual permerror. - MidProcess
-
The verifier is looking at a partial view of the chain, with higher signatures stripped (as the validator does walking the chain top-down), so the highest remaining signature is not the true top and the checks that apply only to the top are suppressed. Implies
AllowUnsignedMI. - StreamBody
-
On (the default): hash every Message-Instance's body as the body arrives, in one pass (Mail::DKIM2::BodyPlan), and every header hash from the fields as received (Mail::DKIM2::HeaderPlan); the body is never held and no previous version of the message is rebuilt. Off (
0): keep the body and undo each instance in turn. The verdicts, and their words, are the same. - HeadersOnly
-
The message has no body, as with the returned original in a DSN's
text/rfc822-headerspart (spec-06 section 12.1.2). Signatures and the chain are checked as usual; of the Message-Instance content check, only the top instance's header hash can be, so only that is.
An option not listed here croaks.
METHODS
PRINT($bytes), CLOSE(), load($input)
Feed the message; see Mail::DKIM2::HeaderParser.
result()
One of:
pass-
Every signature verified, the chain is complete and consistent, and the Message-Instance chain undoes cleanly.
fail-
A signature did not verify, the chain has a gap or a chain-of-custody mismatch, a Message-Instance does not match the content, or a Recipe did not undo.
none-
No DKIM2-Signature headers.
permerror-
The message is malformed in a way no retry will fix: too many fields, a repeated number, a duplicate tag, an unsigned Message-Instance, a required tag missing, a Message-Instance whose hash sets do not parse, a key too short.
temperror-
A public key could not be fetched for a transient reason. Retry later.
Before CLOSE this is none.
details()
The reason, with no result word wrapped around it, e.g. "i=1..3 verified" or "missing DKIM2-Signature i=2"; undef when there is none. Use this when embedding the reason in something that already states the result, such as an Authentication-Results comment.
result_detail()
result and details together, e.g. "pass (i=1..3 verified)".
signatures()
The DKIM2-Signature headers the message carried, as Mail::DKIM2::Signature objects in ascending i= order.
top_signature()
The highest-i= signature, or undef. Its domain and sequence are header.d and header.i for Authentication-Results.
fetch_public_key($signature, $index)
The default key source: a TXT lookup of <selector>._domainkey.<d=> through the Resolver (the answer to the background lookup sent at the end of the headers, if there was one; see Resolver). Each name is looked up once per message. Returns a key object, undef when the answer positively says there is no such record (NXDOMAIN, NOERROR or NODATA), and dies with a TEMPERROR: message for anything else, including a resolver error string it has never seen. The verifier maps that die to temperror: spec-06 section 10 makes a DNS failure retryable, never a fail, which would read as a forged signature.
A record that is present but unusable dies with PERMERROR: <why>, which the verifier reports as permerror naming the selector (spec-06 section 11.5): has multiple records (more than one TXT RR; several strings in one RR are joined), has a syntax error and has been revoked (see "parse_dkim_key_record" in Mail::DKIM2::Common), or algorithm mismatch (a k= that is not the signature algorithm's).
resolver([$resolver]), set_pubkey_callback(\&cb), skip_timestamp_check([$bool]), allow_unsigned_mi([$bool]), next_domain_ok([$domain]), mid_process([$bool]), headers_only([$bool]), stream_body([$bool])
Get or set the constructor options of the same names.
VERIFICATION PROCESS
Shape. At most 32 Message-Instance and 32 DKIM2-Signature fields, every DKIM2-Signature parses with an
i=that is a positive integer (one that does not is never skipped: "PERMERROR DKIM2-Signature has a missing or malformed i= tag"), everyi=andm=a chain number (1*DIGIT, at most three digits naming 1 to 100, and no more than 32;01is 1), noi=orm=twice, and no Message-Instance above the highest signedm=. Any of these is apermerrordecided from the headers alone, before any key is fetched. A tag repeated within one signature is apermerrorfound when that signature is checked.Chain completeness.
i=1toi=Nwith no gaps; likewisem=1to the highest instance.Each signature. The signing input for
i=Kis the canonicalized Message-Instance headers up to them=it names, the DKIM2-Signature headers below it, and itself with an emptys=value, in that order (spec-06 section 8.5). Every item ins=whose algorithm is known and whose key can be fetched must verify; an item whose key is absent is skipped if another item verifies.Chain of custody. For consecutive hops, the
mf=domain ofi=Kmust be the same as or below art=domain ofi=K-1; annd=hop instead names thed=of the hop that follows (spec-06 sections 9.3 and 11.4).Flags. A hop that changed the message after a
donotmodify, or exploded it after adonotexplode, is afail.Content. The top Message-Instance must match the message; its Recipe is applied and the next instance down checked against the result, back to
m=1; past an instance with a null body Recipe the walk continues over the header history only.
AUTHOR
Bron Gondwana <brong@fastmailteam.com>
COPYRIGHT AND LICENSE
Copyright (c) 2025-2026 Fastmail Pty Ltd. This is free software; you can redistribute it and/or modify it under the same terms as Perl itself.