File Coverage

blib/lib/Mail/SpamAssassin/Plugin/DKIM.pm
Criterion Covered Total %
statement 107 503 21.2
branch 22 336 6.5
condition 4 123 3.2
subroutine 15 35 42.8
pod 1 14 7.1
total 149 1011 14.7


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