← Index
NYTProf Performance Profile   « line view »
For /usr/local/bin/sa-learn
  Run on Tue Nov 7 05:38:10 2017
Reported on Tue Nov 7 06:16:04 2017

Filename/usr/local/lib/perl5/site_perl/Mail/SpamAssassin/Plugin/DKIM.pm
StatementsExecuted 1727 statements in 22.9ms
Subroutines
Calls P F Exclusive
Time
Inclusive
Time
Subroutine
83114.98ms15.7msMail::SpamAssassin::Plugin::DKIM::::__ANON__[:420]Mail::SpamAssassin::Plugin::DKIM::__ANON__[:420]
85114.51ms5.13msMail::SpamAssassin::Plugin::DKIM::::__ANON__[:468]Mail::SpamAssassin::Plugin::DKIM::__ANON__[:468]
336411.17ms1.17msMail::SpamAssassin::Plugin::DKIM::::CORE:matchMail::SpamAssassin::Plugin::DKIM::CORE:match (opcode)
16611432µs432µsMail::SpamAssassin::Plugin::DKIM::::CORE:substcontMail::SpamAssassin::Plugin::DKIM::CORE:substcont (opcode)
8311400µs400µsMail::SpamAssassin::Plugin::DKIM::::CORE:substMail::SpamAssassin::Plugin::DKIM::CORE:subst (opcode)
111243µs937µsMail::SpamAssassin::Plugin::DKIM::::newMail::SpamAssassin::Plugin::DKIM::new
11170µs330µsMail::SpamAssassin::Plugin::DKIM::::set_configMail::SpamAssassin::Plugin::DKIM::set_config
11155µs55µsMail::SpamAssassin::Plugin::DKIM::::BEGIN@122Mail::SpamAssassin::Plugin::DKIM::BEGIN@122
11132µs40µsMail::SpamAssassin::Plugin::DKIM::::BEGIN@126Mail::SpamAssassin::Plugin::DKIM::BEGIN@126
11131µs114µsMail::SpamAssassin::Plugin::DKIM::::BEGIN@131Mail::SpamAssassin::Plugin::DKIM::BEGIN@131
11131µs36µsMail::SpamAssassin::Plugin::DKIM::::BEGIN@128Mail::SpamAssassin::Plugin::DKIM::BEGIN@128
11127µs249µsMail::SpamAssassin::Plugin::DKIM::::BEGIN@123Mail::SpamAssassin::Plugin::DKIM::BEGIN@123
11125µs111µsMail::SpamAssassin::Plugin::DKIM::::BEGIN@129Mail::SpamAssassin::Plugin::DKIM::BEGIN@129
11121µs66µsMail::SpamAssassin::Plugin::DKIM::::BEGIN@127Mail::SpamAssassin::Plugin::DKIM::BEGIN@127
11119µs19µsMail::SpamAssassin::Plugin::DKIM::::BEGIN@124Mail::SpamAssassin::Plugin::DKIM::BEGIN@124
0000s0sMail::SpamAssassin::Plugin::DKIM::::__ANON__[:1060]Mail::SpamAssassin::Plugin::DKIM::__ANON__[:1060]
0000s0sMail::SpamAssassin::Plugin::DKIM::::__ANON__[:400]Mail::SpamAssassin::Plugin::DKIM::__ANON__[:400]
0000s0sMail::SpamAssassin::Plugin::DKIM::::__ANON__[:442]Mail::SpamAssassin::Plugin::DKIM::__ANON__[:442]
0000s0sMail::SpamAssassin::Plugin::DKIM::::__ANON__[:833]Mail::SpamAssassin::Plugin::DKIM::__ANON__[:833]
0000s0sMail::SpamAssassin::Plugin::DKIM::::_check_dkim_adspMail::SpamAssassin::Plugin::DKIM::_check_dkim_adsp
0000s0sMail::SpamAssassin::Plugin::DKIM::::_check_dkim_signatureMail::SpamAssassin::Plugin::DKIM::_check_dkim_signature
0000s0sMail::SpamAssassin::Plugin::DKIM::::_check_dkim_signed_byMail::SpamAssassin::Plugin::DKIM::_check_dkim_signed_by
0000s0sMail::SpamAssassin::Plugin::DKIM::::_check_dkim_whitelistMail::SpamAssassin::Plugin::DKIM::_check_dkim_whitelist
0000s0sMail::SpamAssassin::Plugin::DKIM::::_dkim_load_modulesMail::SpamAssassin::Plugin::DKIM::_dkim_load_modules
0000s0sMail::SpamAssassin::Plugin::DKIM::::_get_authorsMail::SpamAssassin::Plugin::DKIM::_get_authors
0000s0sMail::SpamAssassin::Plugin::DKIM::::_wlcheck_acceptable_signatureMail::SpamAssassin::Plugin::DKIM::_wlcheck_acceptable_signature
0000s0sMail::SpamAssassin::Plugin::DKIM::::_wlcheck_author_signatureMail::SpamAssassin::Plugin::DKIM::_wlcheck_author_signature
0000s0sMail::SpamAssassin::Plugin::DKIM::::_wlcheck_listMail::SpamAssassin::Plugin::DKIM::_wlcheck_list
0000s0sMail::SpamAssassin::Plugin::DKIM::::check_dkim_adspMail::SpamAssassin::Plugin::DKIM::check_dkim_adsp
0000s0sMail::SpamAssassin::Plugin::DKIM::::check_dkim_dependableMail::SpamAssassin::Plugin::DKIM::check_dkim_dependable
0000s0sMail::SpamAssassin::Plugin::DKIM::::check_dkim_signallMail::SpamAssassin::Plugin::DKIM::check_dkim_signall
0000s0sMail::SpamAssassin::Plugin::DKIM::::check_dkim_signedMail::SpamAssassin::Plugin::DKIM::check_dkim_signed
0000s0sMail::SpamAssassin::Plugin::DKIM::::check_dkim_signsomeMail::SpamAssassin::Plugin::DKIM::check_dkim_signsome
0000s0sMail::SpamAssassin::Plugin::DKIM::::check_dkim_testingMail::SpamAssassin::Plugin::DKIM::check_dkim_testing
0000s0sMail::SpamAssassin::Plugin::DKIM::::check_dkim_validMail::SpamAssassin::Plugin::DKIM::check_dkim_valid
0000s0sMail::SpamAssassin::Plugin::DKIM::::check_dkim_valid_author_sigMail::SpamAssassin::Plugin::DKIM::check_dkim_valid_author_sig
0000s0sMail::SpamAssassin::Plugin::DKIM::::check_dkim_verifiedMail::SpamAssassin::Plugin::DKIM::check_dkim_verified
0000s0sMail::SpamAssassin::Plugin::DKIM::::check_for_def_dkim_whitelist_fromMail::SpamAssassin::Plugin::DKIM::check_for_def_dkim_whitelist_from
0000s0sMail::SpamAssassin::Plugin::DKIM::::check_for_dkim_whitelist_fromMail::SpamAssassin::Plugin::DKIM::check_for_dkim_whitelist_from
Call graph for these subroutines as a Graphviz dot language file.
Line State
ments
Time
on line
Calls Time
in subs
Code
1# <@LICENSE>
2# Licensed to the Apache Software Foundation (ASF) under one or more
3# contributor license agreements. See the NOTICE file distributed with
4# this work for additional information regarding copyright ownership.
5# The ASF licenses this file to you under the Apache License, Version 2.0
6# (the "License"); you may not use this file except in compliance with
7# the License. You may obtain a copy of the License at:
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16# </@LICENSE>
17
18=head1 NAME
19
20Mail::SpamAssassin::Plugin::DKIM - perform DKIM verification tests
21
22=head1 SYNOPSIS
23
24 loadplugin Mail::SpamAssassin::Plugin::DKIM [/path/to/DKIM.pm]
25
26Taking into account signatures from any signing domains:
27
28 full DKIM_SIGNED eval:check_dkim_signed()
29 full DKIM_VALID eval:check_dkim_valid()
30 full DKIM_VALID_AU eval:check_dkim_valid_author_sig()
31
32Taking into account signatures from specified signing domains only:
33(quotes may be omitted on domain names consisting only of letters, digits,
34dots, and minus characters)
35
36 full DKIM_SIGNED_MY1 eval:check_dkim_signed('dom1','dom2',...)
37 full DKIM_VALID_MY1 eval:check_dkim_valid('dom1','dom2',...)
38 full DKIM_VALID_AU_MY1 eval:check_dkim_valid_author_sig('d1','d2',...)
39
40 full __DKIM_DEPENDABLE eval:check_dkim_dependable()
41
42Author Domain Signing Practices (ADSP) from any author domains:
43
44 header DKIM_ADSP_NXDOMAIN eval:check_dkim_adsp('N')
45 header DKIM_ADSP_ALL eval:check_dkim_adsp('A')
46 header DKIM_ADSP_DISCARD eval:check_dkim_adsp('D')
47 header DKIM_ADSP_CUSTOM_LOW eval:check_dkim_adsp('1')
48 header DKIM_ADSP_CUSTOM_MED eval:check_dkim_adsp('2')
49 header DKIM_ADSP_CUSTOM_HIGH eval:check_dkim_adsp('3')
50
51Author Domain Signing Practices (ADSP) from specified author domains only:
52
53 header DKIM_ADSP_MY1 eval:check_dkim_adsp('*','dom1','dom2',...)
54
55 describe DKIM_SIGNED Message has a DKIM or DK signature, not necessarily valid
56 describe DKIM_VALID Message has at least one valid DKIM or DK signature
57 describe DKIM_VALID_AU Message has a valid DKIM or DK signature from author's domain
58 describe __DKIM_DEPENDABLE A validation failure not attributable to truncation
59
60 describe DKIM_ADSP_NXDOMAIN Domain not in DNS and no valid author domain signature
61 describe DKIM_ADSP_ALL Domain signs all mail, no valid author domain signature
62 describe DKIM_ADSP_DISCARD Domain signs all mail and suggests discarding mail with no valid author domain signature, no valid author domain signature
63 describe DKIM_ADSP_CUSTOM_LOW adsp_override is CUSTOM_LOW, no valid author domain signature
64 describe DKIM_ADSP_CUSTOM_MED adsp_override is CUSTOM_MED, no valid author domain signature
65 describe DKIM_ADSP_CUSTOM_HIGH adsp_override is CUSTOM_HIGH, no valid author domain signature
66
67For compatibility with pre-3.3.0 versions, the following are synonyms:
68
69 OLD: eval:check_dkim_verified = NEW: eval:check_dkim_valid
70 OLD: eval:check_dkim_signall = NEW: eval:check_dkim_adsp('A')
71 OLD: eval:check_dkim_signsome = NEW: redundant, semantically always true
72
73The __DKIM_DEPENDABLE eval rule deserves an explanation. The rule yields true
74when signatures are supplied by a caller, OR ELSE when signatures are obtained
75by this plugin AND either there are no signatures OR a rule __TRUNCATED was
76false. In other words: __DKIM_DEPENDABLE is true when failed signatures can
77not be attributed to message truncation when feeding a message to SpamAssassin.
78It can be consulted to prevent false positives on large but truncated messages
79with poor man's implementation of ADSP by hand-crafted rules.
80
81=head1 DESCRIPTION
82
83This SpamAssassin plugin implements DKIM lookups as described by the RFC 4871,
84as well as historical DomainKeys lookups, as described by RFC 4870, thanks
85to the support for both types of signatures by newer versions of module
86Mail::DKIM.
87
88It requires the C<Mail::DKIM> CPAN module to operate. Many thanks to Jason Long
89for that module.
90
91=head1 TAGS
92
93The following tags are added to the set, available for use in reports,
94header fields, other plugins, etc.:
95
96 _DKIMIDENTITY_
97 Agent or User Identifier (AUID) (the 'i' tag) from valid signatures;
98
99 _DKIMDOMAIN_
100 Signing Domain Identifier (SDID) (the 'd' tag) from valid signatures;
101
102Identities and domains from signatures which failed verification are not
103included in these tags. Duplicates are eliminated (e.g. when there are two or
104more valid signatures from the same signer, only one copy makes it into a tag).
105Note that there may be more than one signature in a message - currently they
106are provided as a space-separated list, although this behaviour may change.
107
108=head1 SEE ALSO
109
110C<Mail::DKIM>, C<Mail::SpamAssassin::Plugin>
111
112 http://jason.long.name/dkimproxy/
113 http://tools.ietf.org/rfc/rfc4871.txt
114 http://tools.ietf.org/rfc/rfc4870.txt
115 http://tools.ietf.org/rfc/rfc5617.txt
116 http://ietf.org/html.charters/dkim-charter.html
117
118=cut
119
120package Mail::SpamAssassin::Plugin::DKIM;
121
122291µs155µs
# spent 55µs within Mail::SpamAssassin::Plugin::DKIM::BEGIN@122 which was called: # once (55µs+0s) by Mail::SpamAssassin::PluginHandler::load_plugin at line 122
use Mail::SpamAssassin::Plugin;
# spent 55µs making 1 call to Mail::SpamAssassin::Plugin::DKIM::BEGIN@122
123264µs2471µs
# spent 249µs (27+222) within Mail::SpamAssassin::Plugin::DKIM::BEGIN@123 which was called: # once (27µs+222µs) by Mail::SpamAssassin::PluginHandler::load_plugin at line 123
use Mail::SpamAssassin::Logger;
# spent 249µs making 1 call to Mail::SpamAssassin::Plugin::DKIM::BEGIN@123 # spent 222µs making 1 call to Exporter::import
124281µs119µs
# spent 19µs within Mail::SpamAssassin::Plugin::DKIM::BEGIN@124 which was called: # once (19µs+0s) by Mail::SpamAssassin::PluginHandler::load_plugin at line 124
use Mail::SpamAssassin::Timeout;
# spent 19µs making 1 call to Mail::SpamAssassin::Plugin::DKIM::BEGIN@124
125
126272µs248µs
# spent 40µs (32+8) within Mail::SpamAssassin::Plugin::DKIM::BEGIN@126 which was called: # once (32µs+8µs) by Mail::SpamAssassin::PluginHandler::load_plugin at line 126
use strict;
# spent 40µs making 1 call to Mail::SpamAssassin::Plugin::DKIM::BEGIN@126 # spent 8µs making 1 call to strict::import
127262µs2111µs
# spent 66µs (21+45) within Mail::SpamAssassin::Plugin::DKIM::BEGIN@127 which was called: # once (21µs+45µs) by Mail::SpamAssassin::PluginHandler::load_plugin at line 127
use warnings;
# spent 66µs making 1 call to Mail::SpamAssassin::Plugin::DKIM::BEGIN@127 # spent 45µs making 1 call to warnings::import
128278µs241µs
# spent 36µs (31+5) within Mail::SpamAssassin::Plugin::DKIM::BEGIN@128 which was called: # once (31µs+5µs) by Mail::SpamAssassin::PluginHandler::load_plugin at line 128
use bytes;
# spent 36µs making 1 call to Mail::SpamAssassin::Plugin::DKIM::BEGIN@128 # spent 5µs making 1 call to bytes::import
129278µs2196µs
# spent 111µs (25+85) within Mail::SpamAssassin::Plugin::DKIM::BEGIN@129 which was called: # once (25µs+85µs) by Mail::SpamAssassin::PluginHandler::load_plugin at line 129
use re 'taint';
# spent 111µs making 1 call to Mail::SpamAssassin::Plugin::DKIM::BEGIN@129 # spent 86µs making 1 call to re::import
130
131210.5ms2197µs
# spent 114µs (31+83) within Mail::SpamAssassin::Plugin::DKIM::BEGIN@131 which was called: # once (31µs+83µs) by Mail::SpamAssassin::PluginHandler::load_plugin at line 131
use vars qw(@ISA);
# spent 114µs making 1 call to Mail::SpamAssassin::Plugin::DKIM::BEGIN@131 # spent 83µs making 1 call to vars::import
132118µs@ISA = qw(Mail::SpamAssassin::Plugin);
133
134# constructor: register the eval rule
135
# spent 937µs (243+694) within Mail::SpamAssassin::Plugin::DKIM::new which was called: # once (243µs+694µs) by Mail::SpamAssassin::PluginHandler::load_plugin at line 1 of (eval 81)[Mail/SpamAssassin/PluginHandler.pm:129]
sub new {
13613µs my $class = shift;
13712µs my $mailsaobject = shift;
138
13912µs $class = ref($class) || $class;
140111µs128µs my $self = $class->SUPER::new($mailsaobject);
# spent 28µs making 1 call to Mail::SpamAssassin::Plugin::new
14112µs bless ($self, $class);
142
143 # signatures
144121µs135µs $self->register_eval_rule("check_dkim_signed");
# spent 35µs making 1 call to Mail::SpamAssassin::Plugin::register_eval_rule
14516µs128µs $self->register_eval_rule("check_dkim_valid");
# spent 28µs making 1 call to Mail::SpamAssassin::Plugin::register_eval_rule
14616µs138µs $self->register_eval_rule("check_dkim_valid_author_sig");
# spent 38µs making 1 call to Mail::SpamAssassin::Plugin::register_eval_rule
14716µs127µs $self->register_eval_rule("check_dkim_testing");
# spent 27µs making 1 call to Mail::SpamAssassin::Plugin::register_eval_rule
148
149 # author domain signing practices
15016µs119µs $self->register_eval_rule("check_dkim_adsp");
# spent 19µs making 1 call to Mail::SpamAssassin::Plugin::register_eval_rule
151113µs130µs $self->register_eval_rule("check_dkim_dependable");
# spent 30µs making 1 call to Mail::SpamAssassin::Plugin::register_eval_rule
152
153 # whitelisting
154110µs127µs $self->register_eval_rule("check_for_dkim_whitelist_from");
# spent 27µs making 1 call to Mail::SpamAssassin::Plugin::register_eval_rule
15516µs149µs $self->register_eval_rule("check_for_def_dkim_whitelist_from");
# spent 49µs making 1 call to Mail::SpamAssassin::Plugin::register_eval_rule
156
157 # old names (aliases) for compatibility
158114µs137µs $self->register_eval_rule("check_dkim_verified"); # = check_dkim_valid
# spent 37µs making 1 call to Mail::SpamAssassin::Plugin::register_eval_rule
159112µs127µs $self->register_eval_rule("check_dkim_signall"); # = check_dkim_adsp('A')
# spent 27µs making 1 call to Mail::SpamAssassin::Plugin::register_eval_rule
16016µs118µs $self->register_eval_rule("check_dkim_signsome"); # redundant, always false
# spent 18µs making 1 call to Mail::SpamAssassin::Plugin::register_eval_rule
161
162114µs1330µs $self->set_config($mailsaobject->{conf});
# spent 330µs making 1 call to Mail::SpamAssassin::Plugin::DKIM::set_config
163
164116µs return $self;
165}
166
167###########################################################################
168
169
# spent 330µs (70+260) within Mail::SpamAssassin::Plugin::DKIM::set_config which was called: # once (70µs+260µs) by Mail::SpamAssassin::Plugin::DKIM::new at line 162
sub set_config {
17012µs my($self, $conf) = @_;
17112µs my @cmds;
172
173=head1 USER SETTINGS
174
175=over 4
176
177=item whitelist_from_dkim author@example.com [signing-domain]
178
179Works similarly to whitelist_from, except that in addition to matching
180an author address (From) to the pattern in the first parameter, the message
181must also carry a Domain Keys Identified Mail (DKIM) signature made by a
182signing domain (SDID, i.e. the d= tag) that is acceptable to us.
183
184Only one whitelist entry is allowed per line, as in C<whitelist_from_rcvd>.
185Multiple C<whitelist_from_dkim> lines are allowed. File-glob style characters
186are allowed for the From address (the first parameter), just like with
187C<whitelist_from_rcvd>. The second parameter does not accept wildcards.
188
189If no signing-domain parameter is specified, the only acceptable signature
190will be an Author Domain Signature (sometimes called first-party signature)
191which is a signature where the signing domain (SDID) of a signature matches
192the domain of the author's address (i.e. the address in a From header field).
193
194Since this whitelist requires a DKIM check to be made, network tests must
195be enabled.
196
197Examples of whitelisting based on an author domain signature (first-party):
198
199 whitelist_from_dkim joe@example.com
200 whitelist_from_dkim *@corp.example.com
201 whitelist_from_dkim *@*.example.com
202
203Examples of whitelisting based on third-party signatures:
204
205 whitelist_from_dkim jane@example.net example.org
206 whitelist_from_dkim rick@info.example.net example.net
207 whitelist_from_dkim *@info.example.net example.net
208 whitelist_from_dkim *@* remailer.example.com
209
210=item def_whitelist_from_dkim author@example.com [signing-domain]
211
212Same as C<whitelist_from_dkim>, but used for the default whitelist entries
213in the SpamAssassin distribution. The whitelist score is lower, because
214these are often targets for abuse of public mailers which sign their mail.
215
216=item unwhitelist_from_dkim author@example.com [signing-domain]
217
218Removes an email address with its corresponding signing-domain field
219from def_whitelist_from_dkim and whitelist_from_dkim tables, if it exists.
220Parameters to unwhitelist_from_dkim must exactly match the parameters of
221a corresponding whitelist_from_dkim or def_whitelist_from_dkim config
222option which created the entry, for it to be removed (a domain name is
223matched case-insensitively); i.e. if a signing-domain parameter was
224specified in a whitelisting command, it must also be specified in the
225unwhitelisting command.
226
227Useful for removing undesired default entries from a distributed configuration
228by a local or site-specific configuration or by C<user_prefs>.
229
230=item adsp_override domain [signing-practices]
231
232Currently few domains publish their signing practices (RFC 5617 - ADSP),
233partly because the ADSP rfc is rather new, partly because they think
234hardly any recipient bothers to check it, and partly for fear that some
235recipients might lose mail due to problems in their signature validation
236procedures or mail mangling by mailers beyond their control.
237
238Nevertheless, recipients could benefit by knowing signing practices of a
239sending (author's) domain, for example to recognize forged mail claiming
240to be from certain domains which are popular targets for phishing, like
241financial institutions. Unfortunately, as signing practices are seldom
242published or are weak, it is hardly justifiable to look them up in DNS.
243
244To overcome this chicken-or-the-egg problem, the C<adsp_override> mechanism
245allows recipients using SpamAssassin to override published or defaulted
246ADSP for certain domains. This makes it possible to manually specify a
247stronger (or weaker) signing practices than a signing domain is willing
248to publish (explicitly or by default), and also save on a DNS lookup.
249
250Note that ADSP (published or overridden) is only consulted for messages
251which do not contain a valid DKIM signature from the author's domain.
252
253According to RFC 5617, signing practices can be one of the following:
254C<unknown>, C<all> and C<discardable>.
255
256C<unknown>: The domain might sign some or all email - messages from the
257domain may or may not have an Author Domain Signature. This is a default
258if a domain exists in DNS but no ADSP record is found.
259
260C<all>: All mail from the domain is signed with an Author Domain Signature.
261
262C<discardable>: All mail from the domain is signed with an Author Domain
263Signature. Furthermore, if a message arrives without a valid Author Domain
264Signature, the domain encourages the recipient(s) to discard it.
265
266ADSP lookup can also determine that a domain is "out of scope", i.e., the
267domain does not exist (NXDOMAIN) in the DNS.
268
269To override domain's signing practices in a SpamAssassin configuration file,
270specify an C<adsp_override> directive for each sending domain to be overridden.
271
272Its first argument is a domain name. Author's domain is matched against it,
273matching is case insensitive. This is not a regular expression or a file-glob
274style wildcard, but limited wildcarding is still available: if this argument
275starts by a "*." (or is a sole "*"), author's domain matches if it is a
276subdomain (to one or more levels) of the argument. Otherwise (with no leading
277asterisk) the match must be exact (not a subdomain).
278
279An optional second parameter is one of the following keywords
280(case-insensitive): C<nxdomain>, C<unknown>, C<all>, C<discardable>,
281C<custom_low>, C<custom_med>, C<custom_high>.
282
283Absence of this second parameter implies C<discardable>. If a domain is not
284listed by a C<adsp_override> directive nor does it explicitly publish any
285ADSP record, then C<unknown> is implied for valid domains, and C<nxdomain>
286for domains not existing in DNS. (Note: domain validity is only checked with
287versions of Mail::DKIM 0.37 or later (actually since 0.36_5), the C<nxdomain>
288would never turn up with older versions).
289
290The strong setting C<discardable> is useful for domains which are known
291to always sign their mail and to always send it directly to recipients
292(not to mailing lists), and are frequent targets of fishing attempts,
293such as financial institutions. The C<discardable> is also appropriate
294for domains which are known never to send any mail.
295
296When a message does not contain a valid signature by the author's domain
297(the domain in a From header field), the signing practices pertaining
298to author's domain determine which of the following rules fire and
299contributes its score: DKIM_ADSP_NXDOMAIN, DKIM_ADSP_ALL, DKIM_ADSP_DISCARD,
300DKIM_ADSP_CUSTOM_LOW, DKIM_ADSP_CUSTOM_MED, DKIM_ADSP_CUSTOM_HIGH. Not more
301than one of these rules can fire for messages that have one author (but see
302below). The last three can only result from a 'signing-practices' as given
303in a C<adsp_override> directive (not from a DNS lookup), and can serve as
304a convenient means of providing a different score if scores assigned to
305DKIM_ADSP_ALL or DKIM_ADSP_DISCARD are not considered suitable for some
306domains.
307
308RFC 5322 permits a message to have more than one author - multiple addresses
309may be listed in a single From header field. RFC 5617 defines that a message
310with multiple authors has multiple signing domain signing practices, but does
311not prescribe how these should be combined. In presence of multiple signing
312practices, more than one of the DKIM_ADSP_* rules may fire.
313
314As a precaution against firing DKIM_ADSP_* rules when there is a known local
315reason for a signature verification failure, the domain's ADSP is considered
316'unknown' when DNS lookups are disabled or a DNS lookup encountered a temporary
317problem on fetching a public key from the author's domain. Similarly, ADSP
318is considered 'unknown' when this plugin did its own signature verification
319(signatures were not passed to SA by a caller) and a metarule __TRUNCATED was
320triggered, indicating the caller intentionally passed a truncated message to
321SpamAssassin, which was a likely reason for a signature verification failure.
322
323Example:
324
325 adsp_override *.mydomain.example.com discardable
326 adsp_override *.neversends.example.com discardable
327
328 adsp_override ebay.com
329 adsp_override *.ebay.com
330 adsp_override ebay.co.uk
331 adsp_override *.ebay.co.uk
332 adsp_override paypal.com
333 adsp_override *.paypal.com
334 adsp_override amazon.com
335 adsp_override ealerts.bankofamerica.com
336 adsp_override americangreetings.com
337 adsp_override egreetings.com
338 adsp_override bluemountain.com
339 adsp_override hallmark.com all
340 adsp_override *.hallmark.com all
341 adsp_override youtube.com custom_high
342 adsp_override google.com custom_low
343 adsp_override gmail.com custom_low
344 adsp_override googlemail.com custom_low
345 adsp_override yahoo.com custom_low
346 adsp_override yahoo.com.au custom_low
347 adsp_override yahoo.se custom_low
348
349 adsp_override junkmailerkbw0rr.com nxdomain
350 adsp_override junkmailerd2hlsg.com nxdomain
351
352 # effectively disables ADSP network DNS lookups for all other domains:
353 adsp_override * unknown
354
355 score DKIM_ADSP_ALL 2.5
356 score DKIM_ADSP_DISCARD 25
357 score DKIM_ADSP_NXDOMAIN 3
358
359 score DKIM_ADSP_CUSTOM_LOW 1
360 score DKIM_ADSP_CUSTOM_MED 3.5
361 score DKIM_ADSP_CUSTOM_HIGH 8
362
363
364=item dkim_minimum_key_bits n (default: 1024)
365
366The smallest size of a signing key (in bits) for a valid signature to be
367considered for whitelisting. Additionally, the eval function check_dkim_valid()
368will return false on short keys when called with explicitly listed domains,
369and the eval function check_dkim_valid_author_sig() will return false on short
370keys (regardless of its arguments). Setting the option to 0 disables a key
371size check.
372
373Note that the option has no effect when the eval function check_dkim_valid()
374is called with no arguments (like in a rule DKIM_VALID). A mere presence of
375some valid signature on a message has no reputational value (without being
376associated with a particular domain), regardless of its key size - anyone can
377prepend its own signature on a copy of some third party mail and re-send it,
378which makes it no more trustworthy than without such signature. This is also
379a reason for a rule DKIM_VALID to have a near-zero score.
380
381=cut
382
383 push (@cmds, {
384 setting => 'whitelist_from_dkim',
385 type => $Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST,
386 code => sub {
387 my ($self, $key, $value, $line) = @_;
388 local ($1,$2);
389 unless (defined $value && $value !~ /^$/) {
390 return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
391 }
392 unless ($value =~ /^(\S+)(?:\s+(\S+))?$/) {
393 return $Mail::SpamAssassin::Conf::INVALID_VALUE;
394 }
395 my $address = $1;
396 my $sdid = defined $2 ? $2 : ''; # empty implies author domain signature
397 $address =~ s/(\@[^@]*)\z/lc($1)/e; # lowercase the email address domain
398 $self->{parser}->add_to_addrlist_dkim('whitelist_from_dkim',
399 $address, lc $sdid);
400 }
401114µs });
402
403 push (@cmds, {
404 setting => 'def_whitelist_from_dkim',
405 type => $Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST,
406
# spent 15.7ms (4.98+10.7) within Mail::SpamAssassin::Plugin::DKIM::__ANON__[/usr/local/lib/perl5/site_perl/Mail/SpamAssassin/Plugin/DKIM.pm:420] which was called 83 times, avg 189µs/call: # 83 times (4.98ms+10.7ms) by Mail::SpamAssassin::Conf::Parser::parse at line 438 of Mail/SpamAssassin/Conf/Parser.pm, avg 189µs/call
code => sub {
40783387µs my ($self, $key, $value, $line) = @_;
40883285µs local ($1,$2);
40983762µs83221µs unless (defined $value && $value !~ /^$/) {
# spent 221µs making 83 calls to Mail::SpamAssassin::Plugin::DKIM::CORE:match, avg 3µs/call
410 return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
411 }
41283847µs83338µs unless ($value =~ /^(\S+)(?:\s+(\S+))?$/) {
# spent 338µs making 83 calls to Mail::SpamAssassin::Plugin::DKIM::CORE:match, avg 4µs/call
413 return $Mail::SpamAssassin::Conf::INVALID_VALUE;
414 }
41583257µs my $address = $1;
41683187µs my $sdid = defined $2 ? $2 : ''; # empty implies author domain signature
4171662.50ms249832µs $address =~ s/(\@[^@]*)\z/lc($1)/e; # lowercase the email address domain
# spent 432µs making 166 calls to Mail::SpamAssassin::Plugin::DKIM::CORE:substcont, avg 3µs/call # spent 400µs making 83 calls to Mail::SpamAssassin::Plugin::DKIM::CORE:subst, avg 5µs/call
418831.18ms839.28ms $self->{parser}->add_to_addrlist_dkim('def_whitelist_from_dkim',
# spent 9.28ms making 83 calls to Mail::SpamAssassin::Conf::Parser::add_to_addrlist_dkim, avg 112µs/call
419 $address, lc $sdid);
420 }
42118µs });
422
423 push (@cmds, {
424 setting => 'unwhitelist_from_dkim',
425 type => $Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST,
426 code => sub {
427 my ($self, $key, $value, $line) = @_;
428 local ($1,$2);
429 unless (defined $value && $value !~ /^$/) {
430 return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
431 }
432 unless ($value =~ /^(\S+)(?:\s+(\S+))?$/) {
433 return $Mail::SpamAssassin::Conf::INVALID_VALUE;
434 }
435 my $address = $1;
436 my $sdid = defined $2 ? $2 : ''; # empty implies author domain signature
437 $address =~ s/(\@[^@]*)\z/lc($1)/e; # lowercase the email address domain
438 $self->{parser}->remove_from_addrlist_dkim('whitelist_from_dkim',
439 $address, lc $sdid);
440 $self->{parser}->remove_from_addrlist_dkim('def_whitelist_from_dkim',
441 $address, lc $sdid);
442 }
44317µs });
444
445 push (@cmds, {
446 setting => 'adsp_override',
447 type => $Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE,
448
# spent 5.13ms (4.51+613µs) within Mail::SpamAssassin::Plugin::DKIM::__ANON__[/usr/local/lib/perl5/site_perl/Mail/SpamAssassin/Plugin/DKIM.pm:468] which was called 85 times, avg 60µs/call: # 85 times (4.51ms+613µs) by Mail::SpamAssassin::Conf::Parser::parse at line 438 of Mail/SpamAssassin/Conf/Parser.pm, avg 60µs/call
code => sub {
44985391µs my ($self, $key, $value, $line) = @_;
45085271µs local ($1,$2);
45185742µs85221µs unless (defined $value && $value !~ /^$/) {
# spent 221µs making 85 calls to Mail::SpamAssassin::Plugin::DKIM::CORE:match, avg 3µs/call
452 return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
453 }
45485902µs85392µs unless ($value =~ /^ \@? ( [*a-z0-9._-]+ )
# spent 392µs making 85 calls to Mail::SpamAssassin::Plugin::DKIM::CORE:match, avg 5µs/call
455 (?: \s+ (nxdomain|unknown|all|discardable|
456 custom_low|custom_med|custom_high) )?$/ix) {
457 return $Mail::SpamAssassin::Conf::INVALID_VALUE;
458 }
45985299µs my $domain = lc $1; # author's domain
46085187µs my $adsp = $2; # author domain signing practices
46185148µs $adsp = 'discardable' if !defined $adsp;
46285173µs $adsp = lc $adsp;
46385317µs if ($adsp eq 'custom_low' ) { $adsp = '1' }
4643257µs elsif ($adsp eq 'custom_med' ) { $adsp = '2' }
46512µs elsif ($adsp eq 'custom_high') { $adsp = '3' }
46652142µs else { $adsp = uc substr($adsp,0,1) } # N/U/A/D/1/2/3
467851.60ms $self->{parser}->{conf}->{adsp_override}->{$domain} = $adsp;
468 }
469110µs });
470
471 # minimal signing key size in bits that is acceptable for whitelisting
47218µs push (@cmds, {
473 setting => 'dkim_minimum_key_bits',
474 default => 1024,
475 type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC,
476 });
477
478=back
479
480=head1 ADMINISTRATOR SETTINGS
481
482=over 4
483
484=item dkim_timeout n (default: 5)
485
486How many seconds to wait for a DKIM query to complete, before scanning
487continues without the DKIM result. A numeric value is optionally suffixed
488by a time unit (s, m, h, d, w, indicating seconds (default), minutes, hours,
489days, weeks).
490
491=back
492
493=cut
494
49514µs push (@cmds, {
496 setting => 'dkim_timeout',
497 is_admin => 1,
498 default => 5,
499 type => $Mail::SpamAssassin::Conf::CONF_TYPE_DURATION
500 });
501
502117µs1260µs $conf->{parser}->register_commands(\@cmds);
503}
504
505# ---------------------------------------------------------------------------
506
507sub check_dkim_signed {
508 my ($self, $pms, $full_ref, @acceptable_domains) = @_;
509 $self->_check_dkim_signature($pms) if !$pms->{dkim_checked_signature};
510 my $result = 0;
511 if (!$pms->{dkim_signed}) {
512 # don't bother
513 } elsif (!@acceptable_domains) {
514 $result = 1; # no additional constraints, any signing domain will do
515 } else {
516 $result = $self->_check_dkim_signed_by($pms,0,0,\@acceptable_domains);
517 }
518 return $result;
519}
520
521sub check_dkim_valid {
522 my ($self, $pms, $full_ref, @acceptable_domains) = @_;
523 $self->_check_dkim_signature($pms) if !$pms->{dkim_checked_signature};
524 my $result = 0;
525 if (!$pms->{dkim_valid}) {
526 # don't bother
527 } elsif (!@acceptable_domains) {
528 $result = 1; # no additional constraints, any signing domain will do,
529 # also any signing key size will do
530 } else {
531 $result = $self->_check_dkim_signed_by($pms,1,0,\@acceptable_domains);
532 }
533 return $result;
534}
535
536sub check_dkim_valid_author_sig {
537 my ($self, $pms, $full_ref, @acceptable_domains) = @_;
538 $self->_check_dkim_signature($pms) if !$pms->{dkim_checked_signature};
539 my $result = 0;
540 if (!%{$pms->{dkim_has_valid_author_sig}}) {
541 # don't bother
542 } else {
543 $result = $self->_check_dkim_signed_by($pms,1,1,\@acceptable_domains);
544 }
545 return $result;
546}
547
548sub check_dkim_dependable {
549 my ($self, $pms) = @_;
550 $self->_check_dkim_signature($pms) if !$pms->{dkim_checked_signature};
551 return $pms->{dkim_signatures_dependable};
552}
553
554# mosnomer, old synonym for check_dkim_valid, kept for compatibility
555sub check_dkim_verified {
556 return check_dkim_valid(@_);
557}
558
559# no valid Author Domain Signature && ADSP matches the argument
560sub check_dkim_adsp {
561 my ($self, $pms, $adsp_char, @domains_list) = @_;
562 $self->_check_dkim_signature($pms) if !$pms->{dkim_checked_signature};
563 my $result = 0;
564 if (!$pms->{dkim_signatures_ready}) {
565 # don't bother
566 } else {
567 $self->_check_dkim_adsp($pms) if !$pms->{dkim_checked_adsp};
568
569 # an asterisk indicates any ADSP type can match (as long as
570 # there is no valid author domain signature present)
571 $adsp_char = 'NAD123' if $adsp_char eq '*'; # a shorthand for NAD123
572
573 if ( !(grep { index($adsp_char,$_) >= 0 } values %{$pms->{dkim_adsp}}) ) {
574 # not the right ADSP type
575 } elsif (!@domains_list) {
576 $result = 1; # no additional constraints, any author domain will do
577 } else {
578 local $1;
579 my %author_domains = %{$pms->{dkim_author_domains}};
580 foreach my $dom (@domains_list) {
581 if ($dom =~ /^\*?\.(.*)\z/s) { # domain itself or its subdomain
582 my $doms = lc $1;
583 if ($author_domains{$doms} ||
584 (grep { /\.\Q$doms\E\z/s } keys %author_domains) ) {
585 $result = 1; last;
586 }
587 } else { # match on domain (not a subdomain)
588 if ($author_domains{lc $dom}) {
589 $result = 1; last;
590 }
591 }
592 }
593 }
594 }
595 return $result;
596}
597
598# useless, semantically always true according to ADSP (RFC 5617)
599sub check_dkim_signsome {
600 my ($self, $pms) = @_;
601 # the signsome is semantically always true, and thus redundant;
602 # for compatibility just returns false to prevent
603 # a legacy rule DKIM_POLICY_SIGNSOME from always firing
604 return 0;
605}
606
607# synonym with check_dkim_adsp('A'), kept for compatibility
608sub check_dkim_signall {
609 my ($self, $pms) = @_;
610 check_dkim_adsp($self, $pms, 'A');
611}
612
613# public key carries a testing flag
614sub check_dkim_testing {
615 my ($self, $pms) = @_;
616 my $result = 0;
617 $self->_check_dkim_signature($pms) if !$pms->{dkim_checked_signature};
618 $result = 1 if $pms->{dkim_key_testing};
619 return $result;
620}
621
622sub check_for_dkim_whitelist_from {
623 my ($self, $pms) = @_;
624 $self->_check_dkim_whitelist($pms) if !$pms->{whitelist_checked};
625 return $pms->{dkim_match_in_whitelist_from_dkim} ||
626 $pms->{dkim_match_in_whitelist_auth};
627}
628
629sub check_for_def_dkim_whitelist_from {
630 my ($self, $pms) = @_;
631 $self->_check_dkim_whitelist($pms) if !$pms->{whitelist_checked};
632 return $pms->{dkim_match_in_def_whitelist_from_dkim} ||
633 $pms->{dkim_match_in_def_whitelist_auth};
634}
635
636# ---------------------------------------------------------------------------
637
638sub _dkim_load_modules {
639 my ($self) = @_;
640
641 if (!$self->{tried_loading}) {
642 $self->{service_available} = 0;
643 my $timemethod = $self->{main}->UNIVERSAL::can("time_method") &&
644 $self->{main}->time_method("dkim_load_modules");
645 my $eval_stat;
646 eval {
647 # Have to do this so that RPM doesn't find these as required perl modules.
648 { require Mail::DKIM::Verifier }
649 } or do {
650 $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
651 };
652 $self->{tried_loading} = 1;
653
654 if (defined $eval_stat) {
655 dbg("dkim: cannot load Mail::DKIM module, DKIM checks disabled: %s",
656 $eval_stat);
657 } else {
658 my $version = Mail::DKIM::Verifier->VERSION;
659 if ($version >= 0.31) {
660 dbg("dkim: using Mail::DKIM version $version");
661 } else {
662 info("dkim: Mail::DKIM $version is older than the required ".
663 "minimal version 0.31, suggested upgrade to 0.37 or later!");
664 }
665 $self->{service_available} = 1;
666
667 my $adsp_avail =
668 eval { require Mail::DKIM::AuthorDomainPolicy }; # since 0.34
669 if (!$adsp_avail) { # fallback to pre-ADSP policy
670 eval { require Mail::DKIM::DkimPolicy } # ignoring status
671 }
672 }
673 }
674 return $self->{service_available};
675}
676
677# ---------------------------------------------------------------------------
678
679sub _check_dkim_signed_by {
680 my ($self, $pms, $must_be_valid, $must_be_author_domain_signature,
681 $acceptable_domains_ref) = @_;
682 my $result = 0;
683 my $verifier = $pms->{dkim_verifier};
684 my $minimum_key_bits = $pms->{conf}->{dkim_minimum_key_bits};
685 foreach my $sig (@{$pms->{dkim_signatures}}) {
686 next if !defined $sig;
687 if ($must_be_valid) {
688 next if ($sig->UNIVERSAL::can("result") ? $sig : $verifier)
689 ->result ne 'pass';
690 next if $sig->UNIVERSAL::can("check_expiration") &&
691 !$sig->check_expiration;
692 next if $minimum_key_bits && $sig->{_spamassassin_key_size} &&
693 $sig->{_spamassassin_key_size} < $minimum_key_bits;
694 }
695 my $sdid = $sig->domain;
696 next if !defined $sdid; # a signature with a missing required tag 'd' ?
697 $sdid = lc $sdid;
698 if ($must_be_author_domain_signature) {
699 next if !$pms->{dkim_author_domains}->{$sdid};
700 }
701 if (!@$acceptable_domains_ref) {
702 $result = 1;
703 } else {
704 foreach my $ad (@$acceptable_domains_ref) {
705 if ($ad =~ /^\*?\.(.*)\z/s) { # domain itself or its subdomain
706 my $d = lc $1;
707 if ($sdid eq $d || $sdid =~ /\.\Q$d\E\z/s) { $result = 1; last }
708 } else { # match on domain (not a subdomain)
709 if ($sdid eq lc $ad) { $result = 1; last }
710 }
711 }
712 }
713 last if $result;
714 }
715 return $result;
716}
717
718sub _get_authors {
719 my ($self, $pms) = @_;
720
721 # Note that RFC 5322 permits multiple addresses in the From header field,
722 # and according to RFC 5617 such message has multiple authors and hence
723 # multiple "Author Domain Signing Practices". For the time being the
724 # SpamAssassin's get() can only provide a single author!
725
726 my %author_domains; local $1;
727 my @authors = grep { defined $_ } ( $pms->get('from:addr',undef) );
728 for (@authors) {
729 # be tolerant, ignore trailing WSP after a domain name
730 $author_domains{lc $1} = 1 if /\@([^\@]+?)[ \t]*\z/s;
731 }
732 $pms->{dkim_author_addresses} = \@authors; # list of full addresses
733 $pms->{dkim_author_domains} = \%author_domains; # hash of their domains
734}
735
736sub _check_dkim_signature {
737 my ($self, $pms) = @_;
738
739 my $conf = $pms->{conf};
740 my($verifier, @signatures, @valid_signatures);
741
742 $pms->{dkim_checked_signature} = 1; # has this sub already been invoked?
743 $pms->{dkim_signatures_ready} = 0; # have we obtained & verified signatures?
744 $pms->{dkim_signatures_dependable} = 0;
745 # dkim_signatures_dependable =
746 # (signatures supplied by a caller) or
747 # ( (signatures obtained by this plugin) and
748 # (no signatures, or message was not truncated) )
749 $pms->{dkim_signatures} = \@signatures;
750 $pms->{dkim_valid_signatures} = \@valid_signatures;
751 $pms->{dkim_signed} = 0;
752 $pms->{dkim_valid} = 0;
753 $pms->{dkim_key_testing} = 0;
754 # the following hashes are keyed by a signing domain (SDID):
755 $pms->{dkim_author_sig_tempfailed} = {}; # DNS timeout verifying author sign.
756 $pms->{dkim_has_valid_author_sig} = {}; # a valid author domain signature
757 $pms->{dkim_has_any_author_sig} = {}; # valid or invalid author domain sign.
758
759 $self->_get_authors($pms) if !$pms->{dkim_author_addresses};
760
761 my $suppl_attrib = $pms->{msg}->{suppl_attrib};
762 if (defined $suppl_attrib && exists $suppl_attrib->{dkim_signatures}) {
763 # caller of SpamAssassin already supplied DKIM signature objects
764 my $provided_signatures = $suppl_attrib->{dkim_signatures};
765 @signatures = @$provided_signatures if ref $provided_signatures;
766 $pms->{dkim_signatures_ready} = 1;
767 $pms->{dkim_signatures_dependable} = 1;
768 dbg("dkim: signatures provided by the caller, %d signatures",
769 scalar(@signatures));
770 }
771
772 if ($pms->{dkim_signatures_ready}) {
773 # signatures already available and verified
774 } elsif (!$pms->is_dns_available()) {
775 dbg("dkim: signature verification disabled, DNS resolving not available");
776 } elsif (!$self->_dkim_load_modules()) {
777 # Mail::DKIM module not available
778 } else {
779 # signature objects not provided by the caller, must verify for ourselves
780 my $timemethod = $self->{main}->UNIVERSAL::can("time_method") &&
781 $self->{main}->time_method("check_dkim_signature");
782 if (Mail::DKIM::Verifier->VERSION >= 0.40) {
783 my $edns = $conf->{dns_options}->{edns};
784 if ($edns && $edns >= 1024) {
785 # Let Mail::DKIM use our interface to Net::DNS::Resolver.
786 # Only do so if EDNS0 provides a reasonably-sized UDP payload size,
787 # as our interface does not provide a DNS fallback to TCP, unlike
788 # the Net::DNS::Resolver::send which does provide it.
789 my $res = $self->{main}->{resolver};
790 dbg("dkim: providing our own resolver: %s", ref $res);
791 Mail::DKIM::DNS::resolver($res);
792 }
793 }
794 $verifier = Mail::DKIM::Verifier->new;
795 if (!$verifier) {
796 dbg("dkim: cannot create Mail::DKIM::Verifier object");
797 return;
798 }
799 $pms->{dkim_verifier} = $verifier;
800 #
801 # feed content of a message into verifier, using \r\n endings,
802 # required by Mail::DKIM API (see bug 5300)
803 # note: bug 5179 comment 28: perl does silly things on non-Unix platforms
804 # unless we use \015\012 instead of \r\n
805 eval {
806 my $str = $pms->{msg}->get_pristine;
807 $str =~ s/\r?\n/\015\012/sg; # ensure \015\012 ending
808 # feeding large chunks to Mail::DKIM is much faster than line-by-line
809 $verifier->PRINT($str);
810 1;
811 } or do { # intercept die() exceptions and render safe
812 my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
813 dbg("dkim: verification failed, intercepted error: $eval_stat");
814 return 0; # cannot verify message
815 };
816
817 my $timeout = $conf->{dkim_timeout};
818 my $timer = Mail::SpamAssassin::Timeout->new(
819 { secs => $timeout, deadline => $pms->{master_deadline} });
820
821 my $err = $timer->run_and_catch(sub {
822 dbg("dkim: performing public key lookup and signature verification");
823 $verifier->CLOSE(); # the action happens here
824
825 # currently SpamAssassin's parsing is better than Mail::Address parsing,
826 # don't bother fetching $verifier->message_originator->address
827 # to replace what we already have in $pms->{dkim_author_addresses}
828
829 # versions before 0.29 only provided a public interface to fetch one
830 # signature, newer versions allow access to all signatures of a message
831 @signatures = $verifier->UNIVERSAL::can("signatures") ?
832 $verifier->signatures : $verifier->signature;
833 });
834 if ($timer->timed_out()) {
835 dbg("dkim: public key lookup or verification timed out after %s s",
836 $timeout );
837#***
838 # $pms->{dkim_author_sig_tempfailed}->{$_} = 1 for ...
839
840 } elsif ($err) {
841 chomp $err;
842 dbg("dkim: public key lookup or verification failed: $err");
843 }
844 $pms->{dkim_signatures_ready} = 1;
845 if (!@signatures || !$pms->{tests_already_hit}->{'__TRUNCATED'}) {
846 $pms->{dkim_signatures_dependable} = 1;
847 }
848 }
849
850 if ($pms->{dkim_signatures_ready}) {
851 my $sig_result_supported;
852 my $minimum_key_bits = $conf->{dkim_minimum_key_bits};
853 foreach my $signature (@signatures) {
854 # old versions of Mail::DKIM would give undef for an invalid signature
855 next if !defined $signature;
856
857 $sig_result_supported = $signature->UNIVERSAL::can("result_detail");
858 my($info, $valid, $expired);
859 $valid =
860 ($sig_result_supported ? $signature : $verifier)->result eq 'pass';
861 $info = $valid ? 'VALID' : 'FAILED';
862 if ($valid && $signature->UNIVERSAL::can("check_expiration")) {
863 $expired = !$signature->check_expiration;
864 $info .= ' EXPIRED' if $expired;
865 }
866 my $key_size;
867 if ($valid && !$expired && $minimum_key_bits) {
868 $key_size = eval { my $pk = $signature->get_public_key;
869 $pk && $pk->cork && $pk->cork->size * 8 };
870 if ($key_size) {
871 $signature->{_spamassassin_key_size} = $key_size; # stash it for later
872 $info .= " WEAK($key_size)" if $key_size < $minimum_key_bits;
873 }
874 }
875 push(@valid_signatures, $signature) if $valid && !$expired;
876
877 # check if we have a potential Author Domain Signature, valid or not
878 my $d = $signature->domain;
879 if (!defined $d) {
880 # can be undefined on a broken signature with missing required tags
881 } else {
882 $d = lc $d;
883 if ($pms->{dkim_author_domains}->{$d}) { # SDID matches author domain
884 $pms->{dkim_has_any_author_sig}->{$d} = 1;
885 if ($valid && !$expired &&
886 $key_size && $key_size >= $minimum_key_bits) {
887 $pms->{dkim_has_valid_author_sig}->{$d} = 1;
888 } elsif ( ($sig_result_supported ? $signature
889 : $verifier)->result_detail
890 =~ /\b(?:timed out|SERVFAIL)\b/i) {
891 $pms->{dkim_author_sig_tempfailed}->{$d} = 1;
892 }
893 }
894 }
895 if (would_log("dbg","dkim")) {
896 dbg("dkim: %s %s, i=%s, d=%s, s=%s, a=%s, c=%s, %s, %s",
897 $info,
898 $signature->isa('Mail::DKIM::DkSignature') ? 'DK' : 'DKIM',
899 map(!defined $_ ? '(undef)' : $_,
900 $signature->identity, $d, $signature->selector,
901 $signature->algorithm, scalar($signature->canonicalization),
902 $key_size ? "key_bits=$key_size" : (),
903 ($sig_result_supported ? $signature : $verifier)->result ),
904 defined $d && $pms->{dkim_author_domains}->{$d}
905 ? 'matches author domain'
906 : 'does not match author domain',
907 );
908 }
909 }
910 if (@valid_signatures) {
911 $pms->{dkim_signed} = 1;
912 $pms->{dkim_valid} = 1;
913 # let the result stand out more clearly in the log, use uppercase
914 my $sig = $valid_signatures[0];
915 my $sig_res = ($sig_result_supported ? $sig : $verifier)->result_detail;
916 dbg("dkim: signature verification result: %s", uc($sig_res));
917
918 # supply values for both tags
919 my(%seen1, %seen2, @identity_list, @domain_list);
920 @identity_list = grep(defined $_ && $_ ne '' && !$seen1{$_}++,
921 map($_->identity, @valid_signatures));
922 @domain_list = grep(defined $_ && $_ ne '' && !$seen2{$_}++,
923 map($_->domain, @valid_signatures));
924 $pms->set_tag('DKIMIDENTITY',
925 @identity_list == 1 ? $identity_list[0] : \@identity_list);
926 $pms->set_tag('DKIMDOMAIN',
927 @domain_list == 1 ? $domain_list[0] : \@domain_list);
928 } elsif (@signatures) {
929 $pms->{dkim_signed} = 1;
930 my $sig = $signatures[0];
931 my $sig_res =
932 ($sig_result_supported && $sig ? $sig : $verifier)->result_detail;
933 dbg("dkim: signature verification result: %s", uc($sig_res));
934 } else {
935 dbg("dkim: signature verification result: none");
936 }
937 }
938}
939
940sub _check_dkim_adsp {
941 my ($self, $pms) = @_;
942
943 $pms->{dkim_checked_adsp} = 1;
944
945 # a message may have multiple authors (RFC 5322),
946 # and hence multiple signing policies (RFC 5617)
947 $pms->{dkim_adsp} = {}; # a hash: author_domain => adsp
948 my $practices_as_string = '';
949
950 $self->_get_authors($pms) if !$pms->{dkim_author_addresses};
951
952 # collect only fully qualified domain names, allow '-', think of IDN
953 my @author_domains = grep { /.\.[a-z-]{2,}\z/si }
954 keys %{$pms->{dkim_author_domains}};
955
956 my %label =
957 ('D' => 'discardable', 'A' => 'all', 'U' => 'unknown', 'N' => 'nxdomain',
958 '1' => 'custom_low', '2' => 'custom_med', '3' => 'custom_high');
959
960 # must check the message first to obtain signer, domain, and verif. status
961 $self->_check_dkim_signature($pms) if !$pms->{dkim_checked_signature};
962
963 if (!$pms->{dkim_signatures_ready}) {
964 dbg("dkim: adsp not retrieved, signatures not obtained");
965
966 } elsif (!@author_domains) {
967 dbg("dkim: adsp not retrieved, no author f.q. domain name");
968 $practices_as_string = 'no author domains, ignored';
969
970 } else {
971
972 foreach my $author_domain (@author_domains) {
973 my $adsp;
974
975 if ($pms->{dkim_has_valid_author_sig}->{$author_domain}) {
976 # don't fetch adsp when valid
977 # RFC 5617: If a message has an Author Domain Signature, ADSP provides
978 # no benefit relative to that domain since the message is already known
979 # to be compliant with any possible ADSP for that domain. [...]
980 # implementations SHOULD avoid doing unnecessary DNS lookups
981 #
982 dbg("dkim: adsp not retrieved, author domain signature is valid");
983 $practices_as_string = 'valid a. d. signature';
984
985 } elsif ($pms->{dkim_author_sig_tempfailed}->{$author_domain}) {
986 dbg("dkim: adsp ignored, tempfail varifying author domain signature");
987 $practices_as_string = 'pub key tempfailed, ignored';
988
989 } elsif ($pms->{dkim_has_any_author_sig}->{$author_domain} &&
990 !$pms->{dkim_signatures_dependable}) {
991 # the message did have an Author Domain Signature but it wasn't valid;
992 # we also believe the message was truncated just before being passed
993 # to SpamAssassin, which is a likely reason for verification failure,
994 # so we shouldn't take it too harsh with ADSP rules - just pretend
995 # the ADSP was 'unknown'
996 #
997 dbg("dkim: adsp ignored, message was truncated, ".
998 "invalid author domain signature");
999 $practices_as_string = 'truncated, ignored';
1000
1001 } else {
1002 # search the adsp_override list
1003
1004 # for a domain a.b.c.d it searches the hash in the following order:
1005 # a.b.c.d
1006 # *.b.c.d
1007 # *.c.d
1008 # *.d
1009 # *
1010 my $matched_key;
1011 my $p = $pms->{conf}->{adsp_override};
1012 if ($p) {
1013 my @d = split(/\./, $author_domain);
1014 @d = map { shift @d; join('.', '*', @d) } (0..$#d);
1015 for my $key ($author_domain, @d) {
1016 $adsp = $p->{$key};
1017 if (defined $adsp) { $matched_key = $key; last }
1018 }
1019 }
1020
1021 if (defined $adsp) {
1022 dbg("dkim: adsp override for domain %s", $author_domain);
1023 $practices_as_string = 'override';
1024 $practices_as_string .=
1025 " by $matched_key" if $matched_key ne $author_domain;
1026
1027 } elsif (!$pms->is_dns_available()) {
1028 dbg("dkim: adsp not retrieved, DNS resolving not available");
1029
1030 } elsif (!$self->_dkim_load_modules()) {
1031 dbg("dkim: adsp not retrieved, module Mail::DKIM not available");
1032
1033 } else { # do the ADSP DNS lookup
1034 my $timemethod = $self->{main}->UNIVERSAL::can("time_method") &&
1035 $self->{main}->time_method("check_dkim_adsp");
1036
1037 my $practices; # author domain signing practices object
1038 my $timeout = $pms->{conf}->{dkim_timeout};
1039 my $timer = Mail::SpamAssassin::Timeout->new(
1040 { secs => $timeout, deadline => $pms->{master_deadline} });
1041 my $err = $timer->run_and_catch(sub {
1042 eval {
1043 if (Mail::DKIM::AuthorDomainPolicy->UNIVERSAL::can("fetch")) {
1044 dbg("dkim: adsp: performing lookup on _adsp._domainkey.%s",
1045 $author_domain);
1046 # get our Net::DNS::Resolver object
1047 my $res = $self->{main}->{resolver}->get_resolver;
1048 $practices = Mail::DKIM::AuthorDomainPolicy->fetch(
1049 Protocol => "dns", Domain => $author_domain,
1050 DnsResolver => $res);
1051 }
1052 1;
1053 } or do {
1054 # fetching/parsing adsp record may throw error, ignore such s.p.
1055 my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
1056 dbg("dkim: adsp: fetch or parse on domain %s failed: %s",
1057 $author_domain, $eval_stat);
1058 undef $practices;
1059 };
1060 });
1061 if ($timer->timed_out()) {
1062 dbg("dkim: adsp lookup on domain %s timed out after %s seconds",
1063 $author_domain, $timeout);
1064 } elsif ($err) {
1065 chomp $err;
1066 dbg("dkim: adsp lookup on domain %s failed: %s",
1067 $author_domain, $err);
1068 } else {
1069 my $sp; # ADSP: unknown / all / discardable
1070 ($sp) = $practices->policy if $practices;
1071 if (!defined $sp || $sp eq '') { # SERVFAIL or a timeout
1072 dbg("dkim: signing practices on %s unavailable", $author_domain);
1073 $adsp = 'U';
1074 $practices_as_string = 'dns: no result';
1075 } else {
1076 $adsp = $sp eq "unknown" ? 'U' # most common
1077 : $sp eq "all" ? 'A'
1078 : $sp eq "discardable" ? 'D' # ADSP
1079 : $sp eq "strict" ? 'D' # old style SSP
1080 : uc($sp) eq "NXDOMAIN" ? 'N'
1081 : 'U';
1082 $practices_as_string = 'dns: ' . $sp;
1083 }
1084 }
1085 }
1086 }
1087
1088 # is signing practices available?
1089 $pms->{dkim_adsp}->{$author_domain} = $adsp if defined $adsp;
1090
1091 dbg("dkim: adsp result: %s (%s), author domain '%s'",
1092 !defined($adsp) ? '-' : $adsp.'/'.$label{$adsp},
1093 $practices_as_string, $author_domain);
1094 }
1095 }
1096}
1097
1098sub _check_dkim_whitelist {
1099 my ($self, $pms) = @_;
1100
1101 $pms->{whitelist_checked} = 1;
1102
1103 $self->_get_authors($pms) if !$pms->{dkim_author_addresses};
1104
1105 my $authors_str = join(", ", @{$pms->{dkim_author_addresses}});
1106 if ($authors_str eq '') {
1107 dbg("dkim: check_dkim_whitelist: could not find author address");
1108 return;
1109 }
1110
1111 # collect whitelist entries matching the author from all lists
1112 my @acceptable_sdid_tuples;
1113 $self->_wlcheck_acceptable_signature($pms, \@acceptable_sdid_tuples,
1114 'def_whitelist_from_dkim');
1115 $self->_wlcheck_author_signature($pms, \@acceptable_sdid_tuples,
1116 'def_whitelist_auth');
1117 $self->_wlcheck_acceptable_signature($pms, \@acceptable_sdid_tuples,
1118 'whitelist_from_dkim');
1119 $self->_wlcheck_author_signature($pms, \@acceptable_sdid_tuples,
1120 'whitelist_auth');
1121 if (!@acceptable_sdid_tuples) {
1122 dbg("dkim: no wl entries match author %s, no need to verify sigs",
1123 $authors_str);
1124 return;
1125 }
1126
1127 # if the message doesn't pass DKIM validation, it can't pass DKIM whitelist
1128
1129 # trigger a DKIM check;
1130 # continue if one or more signatures are valid or we want the debug info
1131 return unless $self->check_dkim_valid($pms) || would_log("dbg","dkim");
1132 return unless $pms->{dkim_signatures_ready};
1133
1134 # now do all the matching in one go, against all signatures in a message
1135 my($any_match_at_all, $any_match_by_wl_ref) =
1136 _wlcheck_list($self, $pms, \@acceptable_sdid_tuples);
1137
1138 my(@valid,@fail);
1139 foreach my $wl (keys %$any_match_by_wl_ref) {
1140 my $match = $any_match_by_wl_ref->{$wl};
1141 if (defined $match) {
1142 $pms->{"dkim_match_in_$wl"} = 1 if $match;
1143 push(@{$match ? \@valid : \@fail}, "$wl/$match");
1144 }
1145 }
1146 if (@valid) {
1147 dbg("dkim: author %s, WHITELISTED by %s",
1148 $authors_str, join(", ",@valid));
1149 } elsif (@fail) {
1150 dbg("dkim: author %s, found in %s BUT IGNORED",
1151 $authors_str, join(", ",@fail));
1152 } else {
1153 dbg("dkim: author %s, not in any dkim whitelist", $authors_str);
1154 }
1155}
1156
1157# check for verifier-acceptable signatures; an empty (or undefined) signing
1158# domain in a whitelist implies checking for an Author Domain Signature
1159#
1160sub _wlcheck_acceptable_signature {
1161 my ($self, $pms, $acceptable_sdid_tuples_ref, $wl) = @_;
1162 my $wl_ref = $pms->{conf}->{$wl};
1163 foreach my $author (@{$pms->{dkim_author_addresses}}) {
1164 foreach my $white_addr (keys %$wl_ref) {
1165 my $wl_addr_ref = $wl_ref->{$white_addr};
1166 my $re = qr/$wl_addr_ref->{re}/i;
1167 # dbg("dkim: WL %s %s, d: %s", $wl, $white_addr,
1168 # join(", ", map { $_ eq '' ? "''" : $_ } @{$wl_addr_ref->{domain}}));
1169 if ($author =~ $re) {
1170 foreach my $sdid (@{$wl_addr_ref->{domain}}) {
1171 push(@$acceptable_sdid_tuples_ref, [$author,$sdid,$wl,$re]);
1172 }
1173 }
1174 }
1175 }
1176}
1177
1178# use a traditional whitelist_from -style addrlist, the only acceptable DKIM
1179# signature is an Author Domain Signature. Note: don't pre-parse and store
1180# domains; that's inefficient memory-wise and only saves one m//
1181#
1182sub _wlcheck_author_signature {
1183 my ($self, $pms, $acceptable_sdid_tuples_ref, $wl) = @_;
1184 my $wl_ref = $pms->{conf}->{$wl};
1185 foreach my $author (@{$pms->{dkim_author_addresses}}) {
1186 foreach my $white_addr (keys %$wl_ref) {
1187 my $re = $wl_ref->{$white_addr};
1188 # dbg("dkim: WL %s %s", $wl, $white_addr);
1189 if ($author =~ $re) {
1190 push(@$acceptable_sdid_tuples_ref, [$author,undef,$wl,$re]);
1191 }
1192 }
1193 }
1194}
1195
1196sub _wlcheck_list {
1197 my ($self, $pms, $acceptable_sdid_tuples_ref) = @_;
1198
1199 my %any_match_by_wl;
1200 my $any_match_at_all = 0;
1201 my $verifier = $pms->{dkim_verifier};
1202 my $minimum_key_bits = $pms->{conf}->{dkim_minimum_key_bits};
1203
1204 # walk through all signatures present in a message
1205 foreach my $signature (@{$pms->{dkim_signatures}}) {
1206 # old versions of Mail::DKIM would give undef for an invalid signature
1207 next if !defined $signature;
1208
1209 my $sig_result_supported = $signature->UNIVERSAL::can("result_detail");
1210 my($info, $valid, $expired, $key_size_weak);
1211 $valid =
1212 ($sig_result_supported ? $signature : $verifier)->result eq 'pass';
1213 $info = $valid ? 'VALID' : 'FAILED';
1214 if ($valid && $signature->UNIVERSAL::can("check_expiration")) {
1215 $expired = !$signature->check_expiration;
1216 $info .= ' EXPIRED' if $expired;
1217 }
1218 if ($valid && !$expired && $minimum_key_bits) {
1219 my $key_size = $signature->{_spamassassin_key_size};
1220 if ($key_size && $key_size < $minimum_key_bits) {
1221 $info .= " WEAK($key_size)"; $key_size_weak = 1;
1222 }
1223 }
1224
1225 my $sdid = $signature->domain;
1226 $sdid = lc $sdid if defined $sdid;
1227
1228 my %tried_authors;
1229 foreach my $entry (@$acceptable_sdid_tuples_ref) {
1230 my($author, $acceptable_sdid, $wl, $re) = @$entry;
1231 # $re and $wl are here for logging purposes only, $re already checked.
1232 # The $acceptable_sdid is a verifier-acceptable signing domain
1233 # identifier (to be matched against a 'd' tag in signatures).
1234 # When $acceptable_sdid is undef or an empty string it implies
1235 # a check for Author Domain Signature.
1236
1237 local $1;
1238 my $author_domain = $author !~ /\@([^\@]+)\z/s ? '' : lc $1;
1239 $tried_authors{$author} = 1; # for logging purposes
1240
1241 my $matches = 0;
1242 if (!defined $sdid) {
1243 # don't bother, invalid signature with a missing 'd' tag
1244
1245 } elsif (!defined $acceptable_sdid || $acceptable_sdid eq '') {
1246 # An "Author Domain Signature" (sometimes called a first-party
1247 # signature) is a Valid Signature in which the domain name of the
1248 # DKIM signing entity, i.e., the d= tag in the DKIM-Signature header
1249 # field, is the same as the domain name in the Author Address.
1250 # Following [RFC5321], domain name comparisons are case insensitive.
1251
1252 # checking for Author Domain Signature
1253 $matches = 1 if $sdid eq $author_domain;
1254
1255 } else { # checking for verifier-acceptable signature
1256 # The second argument to a 'whitelist_from_dkim' option is now (since
1257 # version 3.3.0) supposed to be a signing domain (SDID), no longer an
1258 # identity (AUID). Nevertheless, be prepared to accept the full e-mail
1259 # address there for compatibility, and just ignore its local-part.
1260
1261 $acceptable_sdid = $1 if $acceptable_sdid =~ /\@([^\@]*)\z/;
1262 $matches = 1 if $sdid eq lc $acceptable_sdid;
1263 }
1264 if ($matches) {
1265 if (would_log("dbg","dkim")) {
1266 if ($sdid eq $author_domain) {
1267 dbg("dkim: %s author domain signature by %s, MATCHES %s %s",
1268 $info, $sdid, $wl, $re);
1269 } else {
1270 dbg("dkim: %s third-party signature by %s, author domain %s, ".
1271 "MATCHES %s %s", $info, $sdid, $author_domain, $wl, $re);
1272 }
1273 }
1274 # a defined value indicates at least a match, not necessarily valid
1275 # (this complication servers to preserve logging compatibility)
1276 $any_match_by_wl{$wl} = '' if !exists $any_match_by_wl{$wl};
1277 }
1278 # only valid signature can cause whitelisting
1279 $matches = 0 if !$valid || $expired || $key_size_weak;
1280
1281 if ($matches) {
1282 $any_match_at_all = 1;
1283 $any_match_by_wl{$wl} = $sdid; # value used for debug logging
1284 }
1285 }
1286 dbg("dkim: %s signature by %s, author %s, no valid matches",
1287 $info, defined $sdid ? $sdid : '(undef)',
1288 join(", ", keys %tried_authors)) if !$any_match_at_all;
1289 }
1290 return ($any_match_at_all, \%any_match_by_wl);
1291}
1292
1293110µs1;
 
# spent 1.17ms within Mail::SpamAssassin::Plugin::DKIM::CORE:match which was called 336 times, avg 3µs/call: # 85 times (392µs+0s) by Mail::SpamAssassin::Plugin::DKIM::__ANON__[/usr/local/lib/perl5/site_perl/Mail/SpamAssassin/Plugin/DKIM.pm:468] at line 454, avg 5µs/call # 85 times (221µs+0s) by Mail::SpamAssassin::Plugin::DKIM::__ANON__[/usr/local/lib/perl5/site_perl/Mail/SpamAssassin/Plugin/DKIM.pm:468] at line 451, avg 3µs/call # 83 times (338µs+0s) by Mail::SpamAssassin::Plugin::DKIM::__ANON__[/usr/local/lib/perl5/site_perl/Mail/SpamAssassin/Plugin/DKIM.pm:420] at line 412, avg 4µs/call # 83 times (221µs+0s) by Mail::SpamAssassin::Plugin::DKIM::__ANON__[/usr/local/lib/perl5/site_perl/Mail/SpamAssassin/Plugin/DKIM.pm:420] at line 409, avg 3µs/call
sub Mail::SpamAssassin::Plugin::DKIM::CORE:match; # opcode
# spent 400µs within Mail::SpamAssassin::Plugin::DKIM::CORE:subst which was called 83 times, avg 5µs/call: # 83 times (400µs+0s) by Mail::SpamAssassin::Plugin::DKIM::__ANON__[/usr/local/lib/perl5/site_perl/Mail/SpamAssassin/Plugin/DKIM.pm:420] at line 417, avg 5µs/call
sub Mail::SpamAssassin::Plugin::DKIM::CORE:subst; # opcode
# spent 432µs within Mail::SpamAssassin::Plugin::DKIM::CORE:substcont which was called 166 times, avg 3µs/call: # 166 times (432µs+0s) by Mail::SpamAssassin::Plugin::DKIM::__ANON__[/usr/local/lib/perl5/site_perl/Mail/SpamAssassin/Plugin/DKIM.pm:420] at line 417, avg 3µs/call
sub Mail::SpamAssassin::Plugin::DKIM::CORE:substcont; # opcode