You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spamassassin.apache.org by mm...@apache.org on 2009/09/09 21:49:08 UTC
svn commit: r813095 -
/spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/DKIM.pm
Author: mmartinec
Date: Wed Sep 9 19:49:07 2009
New Revision: 813095
URL: http://svn.apache.org/viewvc?rev=813095&view=rev
Log:
Bug 6189 - DKIM plugin:
draft-ietf-dkim-ssp-10/RFC-5617 made Author Domain Signature
based on 'd':
- updated ADSP code accordingly;
- changed whitelisting code to be based on SDID ('d')
instead of AUID ('i');
- as a mail message may have multiple authors, it can have
multiple author domain signatures from different domains,
and can have multiple author domain signing practices;
change internal data structures and code accordingly;
Modified:
spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/DKIM.pm
Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/DKIM.pm
URL: http://svn.apache.org/viewvc/spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/DKIM.pm?rev=813095&r1=813094&r2=813095&view=diff
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/DKIM.pm (original)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/DKIM.pm Wed Sep 9 19:49:07 2009
@@ -46,9 +46,9 @@
Author Domain Signing Practices (ADSP) from specified author domains only:
header DKIM_ADSP_MY1 eval:check_dkim_adsp('*','dom1','dom2',...)
- describe DKIM_SIGNED Message has a DKIM or DK signature, not necessarily valid
- describe DKIM_VALID Message has at least one valid DKIM or DK signature
- describe DKIM_VALID_AU Message has a valid DKIM or DK signature from author's domain
+ describe DKIM_SIGNED Message has a DKIM or DK signature, not necessarily valid
+ describe DKIM_VALID Message has at least one valid DKIM or DK signature
+ describe DKIM_VALID_AU Message has a valid DKIM or DK signature from author's domain
describe __DKIM_DEPENDABLE A validation failure not attributable to truncation
describe DKIM_ADSP_NXDOMAIN Domain not in DNS and no valid author domain signature
@@ -58,7 +58,7 @@
describe DKIM_ADSP_CUSTOM_MED adsp_override is CUSTOM_MED, no valid author domain signature
describe DKIM_ADSP_CUSTOM_HIGH adsp_override is CUSTOM_HIGH, no valid author domain signature
-For compatibility, the following are synonyms:
+For compatibility with pre-3.3.0 versions, the following are synonyms:
OLD: eval:check_dkim_verified = NEW: eval:check_dkim_valid
OLD: eval:check_dkim_signall = NEW: eval:check_dkim_adsp('A')
OLD: eval:check_dkim_signsome = NEW: redundant, semantically always true
@@ -172,7 +172,7 @@
Use this to supplement the whitelist_from addresses with a check to make
sure the message with a given From address (the author's address) carries
a valid Domain Keys Identified Mail (DKIM) signature by a signing-domain
-(SDID, i.e. the d= tag) that is acceptable to verifier.
+(SDID, i.e. the d= tag) that is acceptable to us (i.e. to the verifier).
Only one whitelist entry is allowed per line, as in C<whitelist_from_rcvd>.
Multiple C<whitelist_from_dkim> lines are allowed. File-glob style characters
@@ -182,7 +182,7 @@
If no signing-domain parameter is specified, the only acceptable signature
will be an Author Domain Signature (sometimes called first-party signature)
which is a signature where the signing domain (SDID) of a signature matches
-the domain of the author address (i.e. the address in a From header field).
+the domain of the author's address (i.e. the address in a From header field).
Since this whitelist requires a DKIM check to be made, network tests must
be enabled.
@@ -195,11 +195,10 @@
Examples of whitelisting based on third-party signatures:
- whitelist_from_dkim rick@example.net richard@example.net
- whitelist_from_dkim rick@sub.example.net example.net
- whitelist_from_dkim jane@example.net example.org
- whitelist_from_dkim *@info.example.com example.com
- whitelist_from_dkim *@* remailer.example.com
+ whitelist_from_dkim jane@example.net example.org
+ whitelist_from_dkim rick@info.example.net example.net
+ whitelist_from_dkim *@info.example.net example.net
+ whitelist_from_dkim *@* remailer.example.com
=item def_whitelist_from_dkim author@example.com [signing-domain]
@@ -264,8 +263,8 @@
listed by a C<adsp_override> directive nor does it explicitly publish any
ADSP record, then C<unknown> is implied for valid domains, and C<nxdomain>
for domains not existing in DNS. (Note: domain validity is only checked with
-versions of Mail::DKIM 0.36_5 or later, C<nxdomain> would never turn up with
-older versions).
+versions of Mail::DKIM 0.37 or later (actually since 0.36_5), the C<nxdomain>
+would never turn up with older versions).
The strong setting C<discardable> is useful for domains which are known
to always sign their mail and to always send it directly to recipients
@@ -278,11 +277,18 @@
to author's domain determine which of the following rules fire and
contributes its score: DKIM_ADSP_NXDOMAIN, DKIM_ADSP_ALL, DKIM_ADSP_DISCARD,
DKIM_ADSP_CUSTOM_LOW, DKIM_ADSP_CUSTOM_MED, DKIM_ADSP_CUSTOM_HIGH. Not more
-than one of these rules can fire. The last three can only result from a
-'signing-practices' as given in a C<adsp_override> directive (not from a
-DNS lookup), and can serve as a convenient means of providing a different
-score if scores assigned to DKIM_ADSP_ALL or DKIM_ADSP_DISCARD are not
-considered suitable for some domains.
+than one of these rules can fire for messages that have one author (but see
+below). The last three can only result from a 'signing-practices' as given
+in a C<adsp_override> directive (not from a DNS lookup), and can serve as
+a convenient means of providing a different score if scores assigned to
+DKIM_ADSP_ALL or DKIM_ADSP_DISCARD are not considered suitable for some
+domains.
+
+RFC 5322 permits a message to have more than one author - multiple addresses
+may be listed in a single From header field. RFC 5617 defines that a message
+with multiple authors has multiple signing domain signing practices, but does
+not prescribe how these should be combined. In presence of multiple signing
+practices, more than one of the DKIM_ADSP_* rules may fire.
As a precaution against firing DKIM_ADSP_* rules when there is a known local
reason for a signature verification failure, the domain's ADSP is considered
@@ -347,9 +353,9 @@
return $Mail::SpamAssassin::Conf::INVALID_VALUE;
}
my $address = $1;
- my $identity = defined $2 ? $2 : ''; # empty implies author domain signature
+ my $sdid = defined $2 ? $2 : ''; # empty implies author domain signature
$self->{parser}->add_to_addrlist_rcvd('whitelist_from_dkim',
- $address, $identity);
+ $address, $sdid);
}
});
@@ -365,9 +371,9 @@
return $Mail::SpamAssassin::Conf::INVALID_VALUE;
}
my $address = $1;
- my $identity = defined $2 ? $2 : ''; # empty implies author domain signature
+ my $sdid = defined $2 ? $2 : ''; # empty implies author domain signature
$self->{parser}->add_to_addrlist_rcvd('def_whitelist_from_dkim',
- $address, $identity);
+ $address, $sdid);
}
});
@@ -453,7 +459,7 @@
my ($self, $pms, $full_ref, @acceptable_domains) = @_;
$self->_check_dkim_signature($pms) if !$pms->{dkim_checked_signature};
my $result = 0;
- if (!$pms->{dkim_has_valid_author_sig}) {
+ if (!%{$pms->{dkim_has_valid_author_sig}}) {
# don't bother
} elsif (!@acceptable_domains) {
$result = 1; # no additional constraints, any signing domain will do
@@ -465,7 +471,7 @@
sub check_dkim_dependable {
my ($self, $pms) = @_;
- $self->_check_dkim_signature($pms) unless $pms->{dkim_checked_signature};
+ $self->_check_dkim_signature($pms) if !$pms->{dkim_checked_signature};
return $pms->{dkim_signatures_dependable};
}
@@ -476,28 +482,32 @@
# no valid Author Domain Signature && ADSP matches the argument
sub check_dkim_adsp {
- my ($self, $pms, $adsp_char, @selected_domains) = @_;
+ my ($self, $pms, $adsp_char, @domains_list) = @_;
$self->_check_dkim_signature($pms) if !$pms->{dkim_checked_signature};
my $result = 0;
- if (!$pms->{dkim_signatures_ready} || $pms->{dkim_has_valid_author_sig}) {
+ if (!$pms->{dkim_signatures_ready}) {
# don't bother
} else {
$self->_check_dkim_adsp($pms) if !$pms->{dkim_checked_adsp};
- if ($adsp_char ne '*' && $pms->{dkim_adsp} ne $adsp_char) {
+ # an asterisk indicates any ADSP type can match (as long as
+ # there is no valid author domain signature present)
+ if ($adsp_char ne '*' &&
+ !(grep { $_ eq $adsp_char} values %{$pms->{dkim_adsp}}) ) {
# not the right ADSP type
- } elsif (!@selected_domains) {
+ } elsif (!@domains_list) {
$result = 1; # no additional constraints, any author domain will do
} else {
- my @author_domains = map { defined($_) && /\@([^\@]*)\z/s ? lc($1) : () }
- ( $pms->{dkim_author_address} );
- foreach my $dom (@selected_domains) {
- if ($dom =~ /^\.(.*)\z/s) { # domain or its subdomain
+ local $1;
+ my %author_domains = %{$pms->{dkim_author_domains}};
+ foreach my $dom (@domains_list) {
+ if ($dom =~ /^\*?\.(.*)\z/s) { # domain itself or its subdomain
my $doms = lc $1;
- if (grep { $_ eq $doms || /\.\Q$doms\E\z/s } @author_domains) {
+ if ($author_domains{$doms} ||
+ (grep { /\.\Q$doms\E\z/s } keys %author_domains) ) {
$result = 1; last;
}
} else { # match on domain (not a subdomain)
- if (grep { $_ eq lc $dom } @author_domains) {
+ if ($author_domains{lc $dom}) {
$result = 1; last;
}
}
@@ -526,21 +536,21 @@
sub check_dkim_testing {
my ($self, $pms) = @_;
my $result = 0;
- $self->_check_dkim_signature($pms) unless $pms->{dkim_checked_signature};
+ $self->_check_dkim_signature($pms) if !$pms->{dkim_checked_signature};
$result = 1 if $pms->{dkim_key_testing};
return $result;
}
sub check_for_dkim_whitelist_from {
my ($self, $pms) = @_;
- $self->_check_dkim_whitelist($pms) unless $pms->{whitelist_checked};
+ $self->_check_dkim_whitelist($pms) if !$pms->{whitelist_checked};
return $pms->{dkim_match_in_whitelist_from_dkim} ||
$pms->{dkim_match_in_whitelist_auth};
}
sub check_for_def_dkim_whitelist_from {
my ($self, $pms) = @_;
- $self->_check_dkim_whitelist($pms) unless $pms->{whitelist_checked};
+ $self->_check_dkim_whitelist($pms) if !$pms->{whitelist_checked};
return $pms->{dkim_match_in_def_whitelist_from_dkim} ||
$pms->{dkim_match_in_def_whitelist_auth};
}
@@ -572,7 +582,7 @@
dbg("dkim: using Mail::DKIM version $version");
} else {
info("dkim: Mail::DKIM $version is older than the required ".
- "minimal version 0.31, suggested upgrade to 0.36_5 or later!");
+ "minimal version 0.31, suggested upgrade to 0.37 or later!");
}
$self->{service_available} = 1;
@@ -592,7 +602,6 @@
my ($self, $pms, $must_be_valid, $must_be_author_domain_signature,
$acceptable_domains_ref) = @_;
my $result = 0;
- my @authors = grep { defined $_ } ( $pms->{dkim_author_address} );
my $verifier = $pms->{dkim_verifier};
foreach my $sig (@{$pms->{dkim_signatures}}) {
next if !defined $sig;
@@ -602,17 +611,16 @@
next if $sig->UNIVERSAL::can("check_expiration") &&
!$sig->check_expiration;
}
- local $1;
- my $d = lc($sig->domain);
+ my $sdid = lc($sig->domain);
if ($must_be_author_domain_signature) {
- next if !grep { /\@([^\@]*)\z/s && lc($1) eq $d } @authors;
+ next if !$pms->{dkim_author_domains}->{$sdid};
}
foreach my $ad (@$acceptable_domains_ref) {
- if ($ad =~ /^\.(.*)\z/s) { # domain or its subdomain
- my $ads = lc $1;
- if ($d eq $ads || $d =~ /\.\Q$ads\E\z/s) { $result = 1; last }
+ if ($ad =~ /^\*?\.(.*)\z/s) { # domain itself or its subdomain
+ my $d = lc $1;
+ if ($sdid eq $d || $sdid =~ /\.\Q$d\E\z/s) { $result = 1; last }
} else { # match on domain (not a subdomain)
- if ($d eq lc $ad) { $result = 1; last }
+ if ($sdid eq lc $ad) { $result = 1; last }
}
}
last if $result;
@@ -620,6 +628,24 @@
return $result;
}
+sub _get_authors {
+ my ($self, $pms) = @_;
+
+ # Note that RFC 5322 permits multiple addresses in the From header field,
+ # and according to RFC 5617 such message has multiple authors and hence
+ # multiple "Author Domain Signing Practices". For the time being the
+ # SpamAssassin's get() can only provide a single author!
+
+ my %author_domains; local $1;
+ my @authors = grep { defined $_ } ( $pms->get('from:addr',undef) );
+ for (@authors) {
+ # be tolerant, ignore trailing WSP after a domain name
+ $author_domains{lc $1} = 1 if /\@([^\@]+?)[ \t]*\z/s;
+ }
+ $pms->{dkim_author_addresses} = \@authors; # list of full addresses
+ $pms->{dkim_author_domains} = \%author_domains; # hash of their domains
+}
+
sub _check_dkim_signature {
my ($self, $pms) = @_;
@@ -631,16 +657,17 @@
# (signatures supplied by a caller) or
# ( (signatures obtained by this plugin) and
# (no signatures, or message was not truncated) )
- $pms->{dkim_author_sig_tempfailed} = 0; # DNS timeout verifying author sign.
$pms->{dkim_signatures} = \@signatures;
$pms->{dkim_valid_signatures} = \@valid_signatures;
$pms->{dkim_signed} = 0;
$pms->{dkim_valid} = 0;
- $pms->{dkim_has_valid_author_sig} = 0;
- $pms->{dkim_has_any_author_sig} = 0; # valid or invalid author domain sign.
$pms->{dkim_key_testing} = 0;
- $pms->{dkim_author_address} =
- $pms->get('from:addr',undef) if !defined $pms->{dkim_author_address};
+ # the following hashes are keyed by a signing domain (SDID):
+ $pms->{dkim_author_sig_tempfailed} = {}; # DNS timeout verifying author sign.
+ $pms->{dkim_has_valid_author_sig} = {}; # a valid author domain signature
+ $pms->{dkim_has_any_author_sig} = {}; # valid or invalid author domain sign.
+
+ $self->_get_authors($pms) if !$pms->{dkim_author_addresses};
my $suppl_attrib = $pms->{msg}->{suppl_attrib};
if (defined $suppl_attrib && exists $suppl_attrib->{dkim_signatures}) {
@@ -695,7 +722,7 @@
# currently SpamAssassin's parsing is better than Mail::Address parsing,
# don't bother fetching $verifier->message_originator->address
- # to replace what we already have in $pms->{dkim_author_address}
+ # to replace what we already have in $pms->{dkim_author_addresses}
# versions before 0.29 only provided a public interface to fetch one
# signature, newer versions allow access to all signatures of a message
@@ -705,7 +732,9 @@
if ($timer->timed_out()) {
dbg("dkim: public key lookup or verification timed out after %s s",
$timeout );
- $pms->{dkim_author_sig_tempfailed} = 1;
+#***
+ # $pms->{dkim_author_sig_tempfailed}->{$_} = 1 for ...
+
} elsif ($err) {
chomp $err;
dbg("dkim: public key lookup or verification failed: $err");
@@ -717,71 +746,46 @@
}
if ($pms->{dkim_signatures_ready}) {
- # ADSP + RFC 5321: localpart is case sensitive, domain is case insensitive
- my $author = $pms->{dkim_author_address};
- # Note that RFC 5322 permits multiple addresses in the From header field,
- # and according to RFC 5617 such message has multiple authors and hence
- # multiple "Author Domain Signing Practices". For the time being we only
- # deal with a single author!
- local($1,$2);
- $author = '' if !defined $author;
- $author = $1 . lc($2) if $author =~ /^(.*)(\@[^\@]*)\z/s;
-
my $sig_result_supported;
foreach my $signature (@signatures) {
# old versions of Mail::DKIM would give undef for an invalid signature
next if !defined $signature;
$sig_result_supported = $signature->UNIVERSAL::can("result_detail");
- #
- # i= Identity of the user or agent (e.g., a mailing list manager) on
- # behalf of which this message is signed (dkim-quoted-printable;
- # OPTIONAL, default is an empty local-part followed by an "@"
- # followed by the domain from the "d=" tag).
- my $identity = $signature->identity;
- $identity = $1 . lc($2) if defined $identity &&
- $identity =~ /^(.*)(\@[^\@]*)\z/s;
my $valid =
($sig_result_supported ? $signature : $verifier)->result eq 'pass';
my $expired = 0;
if ($valid && $signature->UNIVERSAL::can("check_expiration")) {
$expired = !$signature->check_expiration;
}
- # check if we have a potential Author Domain Signature, valid or not
- my $id_matches_author = 0;
- if (!defined $identity || $identity eq '') {
- # identity not provided
- } elsif ($identity =~ /.\@[^\@]*\z/s) { # identity has a localpart
- $id_matches_author = 1 if $author eq $identity;
- } elsif ($author =~ /(\@[^\@]*)?\z/s && defined $1 && $1 eq $identity) {
- # ignoring localpart if identity doesn't have a localpart
- $id_matches_author = 1;
- }
push(@valid_signatures, $signature) if $valid && !$expired;
- if ($id_matches_author) {
- $pms->{dkim_has_any_author_sig} = 1;
+ # check if we have a potential Author Domain Signature, valid or not
+ my $d = lc($signature->domain);
+ if ($pms->{dkim_author_domains}->{$d}) { # SDID matches author domain
+ $pms->{dkim_has_any_author_sig}->{$d} = 1;
if ($valid && !$expired) {
- $pms->{dkim_has_valid_author_sig} = 1;
- } elsif (
- ($sig_result_supported ? $signature : $verifier)->result_detail
- =~ /\b(?:timed out|SERVFAIL)\b/i) {
- $pms->{dkim_author_sig_tempfailed} = 1;
+ $pms->{dkim_has_valid_author_sig}->{$d} = 1;
+ } elsif ( ($sig_result_supported ?$signature :$verifier)->result_detail
+ =~ /\b(?:timed out|SERVFAIL)\b/i) {
+ $pms->{dkim_author_sig_tempfailed}->{$d} = 1;
}
}
- would_log("dbg","dkim") &&
+ if (would_log("dbg","dkim")) {
dbg("dkim: i=%s, d=%s, a=%s, c=%s, %s%s, %s",
- defined $identity ? $identity : 'UNDEF', $signature->domain,
+ $signature->identity, $d,
$signature->algorithm, scalar($signature->canonicalization),
($sig_result_supported ? $signature : $verifier)->result,
!$expired ? '' : ', expired',
- $id_matches_author ? 'matches author' : 'does not match author');
+ $pms->{dkim_author_domains}->{$d} ? 'matches author domain' :
+ 'does not match author domain');
+ }
}
if (@valid_signatures) {
$pms->{dkim_signed} = 1;
$pms->{dkim_valid} = 1;
# let the result stand out more clearly in the log, use uppercase
my $sig = $valid_signatures[0];
- my $sigres = ($sig_result_supported ? $sig : $verifier)->result_detail;
- dbg("dkim: signature verification result: %s", uc($sigres));
+ my $sig_res = ($sig_result_supported ? $sig : $verifier)->result_detail;
+ dbg("dkim: signature verification result: %s", uc($sig_res));
my(%seen1,%seen2);
$pms->set_tag('DKIMIDENTITY',
join(" ", grep { defined($_) && $_ ne '' && !$seen1{$_}++ }
@@ -792,165 +796,167 @@
} elsif (@signatures) {
$pms->{dkim_signed} = 1;
my $sig = $signatures[0];
- my $sigres =
+ my $sig_res =
($sig_result_supported && $sig ? $sig : $verifier)->result_detail;
- dbg("dkim: signature verification result: %s", uc($sigres));
+ dbg("dkim: signature verification result: %s", uc($sig_res));
} else {
dbg("dkim: signature verification result: none");
}
}
}
-sub _lookup_dkim_adsp_override {
- my ($self, $pms, $author_domain) = @_;
- # for a domain a.b.c.d it searches the hash in the following order:
- # a.b.c.d
- # *.b.c.d
- # *.c.d
- # *.d
- # *
- my($adsp,$matched_key);
- my $p = $pms->{conf}->{adsp_override};
- if ($p) {
- my @d = split(/\./, $author_domain);
- @d = map { shift @d; join('.', '*', @d) } (0..$#d);
- for my $key ($author_domain, @d) {
- $adsp = $p->{$key};
- if (defined $adsp) { $matched_key = $key; last }
- };
- }
- return !defined $adsp ? () : ($adsp,$matched_key);
-}
-
sub _check_dkim_adsp {
my ($self, $pms) = @_;
$pms->{dkim_checked_adsp} = 1;
- $pms->{dkim_adsp} = 'U';
- $pms->{dkim_author_address} =
- $pms->get('from:addr',undef) if !defined $pms->{dkim_author_address};
- local $1;
- my $author_domain = $pms->{dkim_author_address};
- $author_domain = '' if !defined $author_domain;
- $author_domain = $author_domain =~ /\@([^\@]*)$/ ? lc $1 : '';
+
+ # a message may have multiple authors (RFC 5322),
+ # and hence multiple signing policies (RFC 5617)
+ $pms->{dkim_adsp} = {}; # a hash: author_domain => adsp
my $practices_as_string = '';
+
+ $self->_get_authors($pms) if !$pms->{dkim_author_addresses};
+
+ # collect only fully qualified domain names, allow '-', think of IDN
+ my @author_domains = grep { /.\.[a-z-]{2,}\z/si }
+ keys %{$pms->{dkim_author_domains}};
+
my %label =
('D' => 'discardable', 'A' => 'all', 'U' => 'unknown', 'N' => 'nxdomain',
'1' => 'custom_low', '2' => 'custom_med', '3' => 'custom_high');
# must check the message first to obtain signer, domain, and verif. status
- $self->_check_dkim_signature($pms) unless $pms->{dkim_checked_signature};
+ $self->_check_dkim_signature($pms) if !$pms->{dkim_checked_signature};
if (!$pms->{dkim_signatures_ready}) {
dbg("dkim: adsp not retrieved, signatures not obtained");
- } elsif ($pms->{dkim_has_valid_author_sig}) { # don't fetch adsp when valid
- # RFC 5617: If a message has an Author Domain Signature, ADSP provides
- # no benefit relative to that domain since the message is already known
- # to be compliant with any possible ADSP for that domain. [...]
- # implementations SHOULD avoid doing unnecessary DNS lookups
- #
- dbg("dkim: adsp not retrieved, author domain signature is valid");
+ } elsif (!@author_domains) {
+ dbg("dkim: adsp not retrieved, no author f.q. domain name");
+ $practices_as_string = 'no author domains, ignored';
- } elsif ($author_domain eq '') { # have mercy, don't claim a NXDOMAIN
- dbg("dkim: adsp not retrieved, no author domain (empty)");
- $practices_as_string = 'empty domain, ignored';
-
- } elsif ($author_domain =~ /^[^.]+$/s) { # have mercy, don't claim NXDOMAIN
- dbg("dkim: adsp not retrieved, author domain not fqdn: $author_domain");
- $practices_as_string = 'not fqdn, ignored';
-
- } elsif ($author_domain !~ /.\.[a-z-]{2,}\z/si) { # allow '-', think of IDN
- dbg("dkim: adsp not retrieved, author domain not a fqdn: %s (%s)",
- $author_domain, $pms->{dkim_author_address});
- $pms->{dkim_adsp} = 'N'; $practices_as_string = 'invalid fqdn, ignored';
-
- } elsif ($pms->{dkim_author_sig_tempfailed}) {
- dbg("dkim: adsp ignored, tempfail varifying author domain signature");
- $practices_as_string = 'pub key tempfailed, ignored';
-
- } elsif ($pms->{dkim_has_any_author_sig} &&
- !$pms->{dkim_signatures_dependable}) {
- # the message did have an Author Domain Signature but it wasn't valid;
- # we also expect the message was truncated just before being passed to
- # SpamAssassin, which is a likely reason for verification failure, so
- # we shouldn't take it too harsh with ADSP rules - just pretend the ADSP
- # was 'unknown'
- #
- dbg("dkim: adsp ignored, message was truncated, ".
- "invalid author domain signature");
- $practices_as_string = 'truncated, ignored';
-
- } elsif (my($adsp,$key) =
- $self->_lookup_dkim_adsp_override($pms,$author_domain)) {
- $pms->{dkim_adsp} = $adsp;
- $practices_as_string = 'override';
- $practices_as_string .= " by $key" if $key ne $author_domain;
+ } else {
- } elsif (!$pms->is_dns_available()) {
- dbg("dkim: adsp not retrieved, DNS resolving not available");
+ foreach my $author_domain (@author_domains) {
+ my $adsp;
- } elsif (!$self->_dkim_load_modules()) {
- dbg("dkim: adsp not retrieved, module Mail::DKIM not available");
+ if ($pms->{dkim_has_valid_author_sig}->{$author_domain}) {
+ # don't fetch adsp when valid
+ # RFC 5617: If a message has an Author Domain Signature, ADSP provides
+ # no benefit relative to that domain since the message is already known
+ # to be compliant with any possible ADSP for that domain. [...]
+ # implementations SHOULD avoid doing unnecessary DNS lookups
+ #
+ dbg("dkim: adsp not retrieved, author domain signature is valid");
+ $practices_as_string = 'valid a. d. signature';
+
+ } elsif ($pms->{dkim_author_sig_tempfailed}->{$author_domain}) {
+ dbg("dkim: adsp ignored, tempfail varifying author domain signature");
+ $practices_as_string = 'pub key tempfailed, ignored';
+
+ } elsif ($pms->{dkim_has_any_author_sig}->{$author_domain} &&
+ !$pms->{dkim_signatures_dependable}) {
+ # the message did have an Author Domain Signature but it wasn't valid;
+ # we also believe the message was truncated just before being passed
+ # to SpamAssassin, which is a likely reason for verification failure,
+ # so we shouldn't take it too harsh with ADSP rules - just pretend
+ # the ADSP was 'unknown'
+ #
+ dbg("dkim: adsp ignored, message was truncated, ".
+ "invalid author domain signature");
+ $practices_as_string = 'truncated, ignored';
- } else {
- my $timemethod = $self->{main}->UNIVERSAL::can("time_method") &&
- $self->{main}->time_method("check_dkim_adsp");
+ } else {
+ # search the adsp_override list
- my $timeout = $pms->{conf}->{dkim_timeout};
- my $timer = Mail::SpamAssassin::Timeout->new({ secs => $timeout });
- my $err = $timer->run_and_catch(sub {
- my $practices; # author domain signing practices
- eval {
- if (Mail::DKIM::AuthorDomainPolicy->UNIVERSAL::can("fetch")) {
- dbg("dkim: adsp: performing _adsp lookup on %s", $author_domain);
- # _adsp._domainkey.domain
- $practices = Mail::DKIM::AuthorDomainPolicy->fetch(
- Protocol => "dns", Domain => $author_domain);
- } else { # fall back to pre-ADSP style
- dbg("dkim: adsp: performing _policy lookup on %s", $author_domain);
- # _policy._domainkey.domain
- $practices = Mail::DKIM::DkimPolicy->fetch(
- Protocol => "dns", Domain => $author_domain);
+ # for a domain a.b.c.d it searches the hash in the following order:
+ # a.b.c.d
+ # *.b.c.d
+ # *.c.d
+ # *.d
+ # *
+ my $matched_key;
+ my $p = $pms->{conf}->{adsp_override};
+ if ($p) {
+ my @d = split(/\./, $author_domain);
+ @d = map { shift @d; join('.', '*', @d) } (0..$#d);
+ for my $key ($author_domain, @d) {
+ $adsp = $p->{$key};
+ if (defined $adsp) { $matched_key = $key; last }
+ }
}
- 1;
- } or do {
- # fetching/parsing adsp record may throw error, ignore such practices
- my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
- dbg("dkim: adsp: fetch or parse on domain %s failed: %s",
- $author_domain,$eval_stat);
- undef $practices;
- };
- if (!$practices) {
- dbg("dkim: signing practices: none");
- } else {
- # ADSP: unknown / all / discardable
- my($sp) = $practices->policy;
- if (!defined $sp || $sp eq '') { # SERVFAIL or a timeout
- dbg("dkim: signing practices: unavailable");
- } else {
- $pms->{dkim_adsp} = $sp eq "unknown" ? 'U' # most common
- : $sp eq "all" ? 'A'
- : $sp eq "discardable" ? 'D' # ADSP
- : $sp eq "strict" ? 'D' # old style SSP
- : uc($sp) eq "NXDOMAIN" ? 'N'
- : 'U';
- $practices_as_string = $sp;
+
+ if (defined $adsp) {
+ dbg("dkim: adsp override for domain %s", $author_domain);
+ $practices_as_string = 'override';
+ $practices_as_string .=
+ " by $matched_key" if $matched_key ne $author_domain;
+
+ } elsif (!$pms->is_dns_available()) {
+ dbg("dkim: adsp not retrieved, DNS resolving not available");
+
+ } elsif (!$self->_dkim_load_modules()) {
+ dbg("dkim: adsp not retrieved, module Mail::DKIM not available");
+
+ } else { # do the ADSP DNS lookup
+ my $timemethod = $self->{main}->UNIVERSAL::can("time_method") &&
+ $self->{main}->time_method("check_dkim_adsp");
+
+ my $practices; # author domain signing practices object
+ my $timeout = $pms->{conf}->{dkim_timeout};
+ my $timer = Mail::SpamAssassin::Timeout->new({ secs => $timeout });
+ my $err = $timer->run_and_catch(sub {
+ eval {
+ if (Mail::DKIM::AuthorDomainPolicy->UNIVERSAL::can("fetch")) {
+ dbg("dkim: adsp: performing lookup on _adsp._domainkey.%s",
+ $author_domain);
+ $practices = Mail::DKIM::AuthorDomainPolicy->fetch(
+ Protocol => "dns", Domain => $author_domain);
+ }
+ 1;
+ } or do {
+ # fetching/parsing adsp record may throw error, ignore such s.p.
+ my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
+ dbg("dkim: adsp: fetch or parse on domain %s failed: %s",
+ $author_domain, $eval_stat);
+ undef $practices;
+ };
+ });
+ if ($timer->timed_out()) {
+ dbg("dkim: adsp lookup on domain %s timed out after %s seconds",
+ $author_domain, $timeout);
+ } elsif ($err) {
+ chomp $err;
+ dbg("dkim: adsp lookup on domain %s failed: %s",
+ $author_domain, $err);
+ } else {
+ my $sp; # ADSP: unknown / all / discardable
+ ($sp) = $practices->policy if $practices;
+ if (!defined $sp || $sp eq '') { # SERVFAIL or a timeout
+ dbg("dkim: signing practices on %s unavailable", $author_domain);
+ $adsp = 'U';
+ $practices_as_string = 'dns: no result';
+ } else {
+ $adsp = $sp eq "unknown" ? 'U' # most common
+ : $sp eq "all" ? 'A'
+ : $sp eq "discardable" ? 'D' # ADSP
+ : $sp eq "strict" ? 'D' # old style SSP
+ : uc($sp) eq "NXDOMAIN" ? 'N'
+ : 'U';
+ $practices_as_string = 'dns: ' . $sp;
+ }
+ }
}
}
- });
- if ($timer->timed_out()) {
- dbg("dkim: adsp lookup timed out after $timeout seconds");
- } elsif ($err) {
- chomp $err;
- dbg("dkim: adsp lookup failed: $err");
+ # is signing practices available?
+ $pms->{dkim_adsp}->{$author_domain} = $adsp if defined $adsp;
+
+ dbg("dkim: adsp result: %s (%s), author domain '%s'",
+ !defined($adsp) ? '-' : $adsp.'/'.$label{$adsp},
+ $practices_as_string, $author_domain);
}
}
-
- dbg("dkim: adsp result: %s (%s), domain %s",
- $pms->{dkim_has_valid_author_sig} ? "accept" : $label{$pms->{dkim_adsp}},
- $practices_as_string, $author_domain);
}
sub _check_dkim_whitelist {
@@ -958,40 +964,40 @@
$pms->{whitelist_checked} = 1;
- my $author = $pms->{dkim_author_address};
- if (!defined $author) {
- $pms->{dkim_author_address} = $author = $pms->get('from:addr',undef);
- }
- if (!defined $author || $author eq '') {
+ $self->_get_authors($pms) if !$pms->{dkim_author_addresses};
+
+ my $authors_str = join(", ", @{$pms->{dkim_author_addresses}});
+ if ($authors_str eq '') {
dbg("dkim: check_dkim_whitelist: could not find author address");
return;
}
# collect whitelist entries matching the author from all lists
- my @acceptable_identity_tuples;
- $self->_wlcheck_acceptable_signature($pms, \@acceptable_identity_tuples,
+ my @acceptable_sdid_tuples;
+ $self->_wlcheck_acceptable_signature($pms, \@acceptable_sdid_tuples,
'def_whitelist_from_dkim');
- $self->_wlcheck_author_signature($pms, \@acceptable_identity_tuples,
+ $self->_wlcheck_author_signature($pms, \@acceptable_sdid_tuples,
'def_whitelist_auth');
- $self->_wlcheck_acceptable_signature($pms, \@acceptable_identity_tuples,
+ $self->_wlcheck_acceptable_signature($pms, \@acceptable_sdid_tuples,
'whitelist_from_dkim');
- $self->_wlcheck_author_signature($pms, \@acceptable_identity_tuples,
+ $self->_wlcheck_author_signature($pms, \@acceptable_sdid_tuples,
'whitelist_auth');
- if (!@acceptable_identity_tuples) {
- dbg("dkim: no wl entries match author $author, no need to verify sigs");
+ if (!@acceptable_sdid_tuples) {
+ dbg("dkim: no wl entries match author %s, no need to verify sigs",
+ $authors_str);
return;
}
# if the message doesn't pass DKIM validation, it can't pass DKIM whitelist
- # trigger a DKIM check so we can get address/identity info;
+ # trigger a DKIM check;
# continue if one or more signatures are valid or we want the debug info
return unless $self->check_dkim_valid($pms) || would_log("dbg","dkim");
return unless $pms->{dkim_signatures_ready};
# now do all the matching in one go, against all signatures in a message
my($any_match_at_all, $any_match_by_wl_ref) =
- _wlcheck_list($self, $pms, \@acceptable_identity_tuples);
+ _wlcheck_list($self, $pms, \@acceptable_sdid_tuples);
my(@valid,@fail);
foreach my $wl (keys %$any_match_by_wl_ref) {
@@ -1002,11 +1008,13 @@
}
}
if (@valid) {
- dbg("dkim: author %s, WHITELISTED by %s", $author, join(", ",@valid));
+ dbg("dkim: author %s, WHITELISTED by %s",
+ $authors_str, join(", ",@valid));
} elsif (@fail) {
- dbg("dkim: author %s, found in %s BUT IGNORED", $author, join(", ",@fail));
+ dbg("dkim: author %s, found in %s BUT IGNORED",
+ $authors_str, join(", ",@fail));
} else {
- dbg("dkim: author %s, not in any dkim whitelist", $author);
+ dbg("dkim: author %s, not in any dkim whitelist", $authors_str);
}
}
@@ -1014,13 +1022,16 @@
# domain in a whitelist implies checking for an Author Domain Signature
#
sub _wlcheck_acceptable_signature {
- my ($self, $pms, $acceptable_identity_tuples_ref, $wl) = @_;
- my $author = $pms->{dkim_author_address};
- foreach my $white_addr (keys %{$pms->{conf}->{$wl}}) {
- my $re = qr/$pms->{conf}->{$wl}->{$white_addr}{re}/i;
- if ($author =~ $re) {
- foreach my $acc_id (@{$pms->{conf}->{$wl}->{$white_addr}{domain}}) {
- push(@$acceptable_identity_tuples_ref, [$acc_id,$wl,$re] );
+ my ($self, $pms, $acceptable_sdid_tuples_ref, $wl) = @_;
+ my $wl_ref = $pms->{conf}->{$wl};
+ foreach my $author (@{$pms->{dkim_author_addresses}}) {
+ foreach my $white_addr (keys %$wl_ref) {
+ my $wl_addr_ref = $wl_ref->{$white_addr};
+ my $re = qr/$wl_addr_ref->{re}/i;
+ if ($author =~ $re) {
+ foreach my $sdid (@{$wl_addr_ref->{domain}}) {
+ push(@$acceptable_sdid_tuples_ref, [$author,$sdid,$wl,$re]);
+ }
}
}
}
@@ -1031,28 +1042,27 @@
# domains; that's inefficient memory-wise and only saves one m//
#
sub _wlcheck_author_signature {
- my ($self, $pms, $acceptable_identity_tuples_ref, $wl) = @_;
- my $author = $pms->{dkim_author_address};
- foreach my $white_addr (keys %{$pms->{conf}->{$wl}}) {
- my $re = $pms->{conf}->{$wl}->{$white_addr};
- if ($author =~ $re) {
- push(@$acceptable_identity_tuples_ref, [undef,$wl,$re] );
+ my ($self, $pms, $acceptable_sdid_tuples_ref, $wl) = @_;
+ my $wl_ref = $pms->{conf}->{$wl};
+ foreach my $author (@{$pms->{dkim_author_addresses}}) {
+ foreach my $white_addr (keys %$wl_ref) {
+ my $re = $wl_ref->{$white_addr};
+ if ($author =~ $re) {
+ push(@$acceptable_sdid_tuples_ref, [$author,undef,$wl,$re]);
+ }
}
}
}
sub _wlcheck_list {
- my ($self, $pms, $acceptable_identity_tuples_ref) = @_;
+ my ($self, $pms, $acceptable_sdid_tuples_ref) = @_;
my %any_match_by_wl;
my $any_match_at_all = 0;
my $verifier = $pms->{dkim_verifier};
- my @signatures = @{$pms->{dkim_signatures}};
- my $author = $pms->{dkim_author_address}; # address in a From header field
- $author = '' if !defined $author;
# walk through all signatures present in a message
- foreach my $signature (@signatures) {
+ foreach my $signature (@{$pms->{dkim_signatures}}) {
# old versions of Mail::DKIM would give undef for an invalid signature
next if !defined $signature;
my $sig_result_supported = $signature->UNIVERSAL::can("result_detail");
@@ -1062,39 +1072,26 @@
if ($valid && $signature->UNIVERSAL::can("check_expiration")) {
$expired = !$signature->check_expiration;
}
- my $identity = $signature->identity;
- local($1,$2);
- if (!defined $identity || $identity eq '') {
- $identity = '@' . $signature->domain;
- dbg("dkim: identity empty, setting to %s", $identity);
- } elsif ($identity !~ /\@/) { # just in case
- $identity = '@' . $identity;
- dbg("dkim: identity with no domain, setting to %s", $identity);
- }
- # split identity into local part and domain
- $identity =~ /^ (.*?) \@ ([^\@]*) $/xs;
- my($identity_mbx, $identity_dom) = ($1,$2);
-
- my $author_matching_part = $author;
- if ($identity =~ /^\@/) { # empty localpart in signing identity
- $author_matching_part =~ s/^.*?(\@[^\@]*)?$/$1/s; # strip localpart
- }
+ my $sdid = lc($signature->domain);
- my $info = ''; # summary info string to be used for logging
- $info .= ($valid ? 'VALID' : 'FAILED') . ($expired ? ' EXPIRED' : '');
- $info .= lc $identity eq lc $author_matching_part ? ' author'
- : ' third-party';
- $info .= " signature by id " . $identity;
+ my $info = $valid ? 'VALID' : 'FAILED';
+ $info .= ' EXPIRED' if $expired;
- foreach my $entry (@$acceptable_identity_tuples_ref) {
- my($acceptable_identity, $wl, $re) = @$entry;
+ my %tried_authors;
+ foreach my $entry (@$acceptable_sdid_tuples_ref) {
+ my($author, $acceptable_sdid, $wl, $re) = @$entry;
# $re and $wl are here for logging purposes only, $re already checked.
- # The $acceptable_identity is a verifier-acceptable signing identity.
- # When $acceptable_identity is undef or an empty string it implies an
- # Author Domain Signature check.
+ # The $acceptable_sdid is a verifier-acceptable signing domain
+ # identifier (to be matched against a 'd' tag in signatures).
+ # When $acceptable_sdid is undef or an empty string it implies
+ # a check for Author Domain Signature.
+
+ local $1;
+ my $author_domain = $author !~ /\@([^\@]+)\z/s ? '' : lc $1;
+ $tried_authors{$author} = 1; # for logging purposes
my $matches = 0;
- if (!defined $acceptable_identity || $acceptable_identity eq '') {
+ if (!defined $acceptable_sdid || $acceptable_sdid eq '') {
# An "Author Domain Signature" (sometimes called a first-party
# signature) is a Valid Signature in which the domain name of the
@@ -1103,27 +1100,30 @@
# Following [RFC5321], domain name comparisons are case insensitive.
# checking for Author Domain Signature
- $matches = 1 if lc $identity eq lc $author_matching_part;
+ $matches = 1 if $sdid eq $author_domain;
}
else { # checking for verifier-acceptable signature
- if ($acceptable_identity !~ /\@/) {
- $acceptable_identity = '@' . $acceptable_identity;
- }
- # split into local part and domain
- $acceptable_identity =~ /^ (.*?) \@ ([^\@]*) $/xs;
- my($accept_id_mbx, $accept_id_dom) = ($1,$2);
-
- # let's take a liberty and compare local parts case-insensitively
- if ($accept_id_mbx ne '') { # local part exists, full id must match
- $matches = 1 if lc $identity eq lc $acceptable_identity;
- } else { # any local part in signing identity is acceptable
- # as long as domain matches or is a subdomain
- $matches = 1 if $identity_dom =~ /(^|\.)\Q$accept_id_dom\E\z/i;
- }
+
+ # The second argument to a 'whitelist_from_dkim' option is now (since
+ # version 3.3.0) supposed to be a signing domain (SDID), no longer an
+ # identity (AUID). Nevertheless, be prepared to accept the full e-mail
+ # address there for compatibility, and just ignore its local-part.
+
+ $acceptable_sdid = $1 if $acceptable_sdid =~ /\@([^\@]*)\z/;
+ $matches = 1 if $sdid eq lc $acceptable_sdid;
}
if ($matches) {
- dbg("dkim: $info, author $author, MATCHES $wl $re");
+ if (would_log("dbg","dkim")) {
+ if ($sdid eq $author_domain) {
+ dbg("dkim: %s author domain signature by %s, MATCHES %s %s",
+ $info, $sdid, $wl, $re);
+ } else {
+ dbg("dkim: %s third-party signature by %s, author domain %s, ".
+ "MATCHES %s %s", $info, $sdid, $author_domain, $wl, $re);
+ }
+ }
# a defined value indicates at least a match, not necessarily valid
+ # (this complication servers to preserve logging compatibility)
$any_match_by_wl{$wl} = '' if !exists $any_match_by_wl{$wl};
}
# only valid signature can cause whitelisting
@@ -1131,10 +1131,11 @@
if ($matches) {
$any_match_at_all = 1;
- $any_match_by_wl{$wl} = $identity; # value used for debug logging
+ $any_match_by_wl{$wl} = $sdid; # value used for debug logging
}
}
- dbg("dkim: $info, author $author, no valid matches") if !$any_match_at_all;
+ dbg("dkim: %s signature by %s, author %s, no valid matches",
+ $info, $sdid, join(", ", keys %tried_authors)) if !$any_match_at_all;
}
return ($any_match_at_all, \%any_match_by_wl);
}