File Coverage

blib/lib/Mail/SpamAssassin/Plugin/DKIM.pm
Criterion Covered Total %
statement 211 511 41.2
branch 73 340 21.4
condition 10 123 8.1
subroutine 21 37 56.7
pod 1 14 7.1
total 316 1025 30.8


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