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. 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 query and errorstring methods) for the default public-key lookup. Created on demand if not given. A milter host passes its own so its timeouts apply.

PubkeyCallback

A code reference that replaces the DNS lookup. Called as ($signature, $index, $verifier) for each item of each signature's s= 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.

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.

HeadersOnly

The message has no body, as with the returned original in a DSN's text/rfc822-headers part (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. 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.

resolver([$resolver]), set_pubkey_callback(\&cb), skip_timestamp_check([$bool]), allow_unsigned_mi([$bool]), mid_process([$bool]), headers_only([$bool])

Get or set the constructor options of the same names.

VERIFICATION PROCESS

  1. Shape. At most 32 Message-Instance and 32 DKIM2-Signature fields, no i= or m= twice, and no Message-Instance above the highest signed m=. Any of these is a permerror decided from the headers alone, before any key is fetched. A tag repeated within one signature is a permerror found when that signature is checked.

  2. Chain completeness. i=1 to i=N with no gaps; likewise m=1 to the highest instance.

  3. Each signature. The signing input for i=K is the canonicalized Message-Instance headers up to the m= it names, the DKIM2-Signature headers below it, and itself with an empty s= value, in that order (spec-06 section 8.5). Every item in s= whose algorithm is known and whose key can be fetched must verify; an item whose key is absent is skipped if another item verifies.

  4. Chain of custody. For consecutive hops, the mf= domain of i=K must be the same as or below a rt= domain of i=K-1; an nd= hop instead names the d= of the hop that follows (spec-06 sections 9.3 and 11.4).

  5. Flags. A hop that changed the message after a donotmodify, or exploded it after a donotexplode, is a fail.

  6. 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 or an instance that declares the previous state unrecoverable.

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.