File Coverage

blib/lib/Mail/SpamAssassin/Plugin/DKIM.pm
Criterion Covered Total %
statement 208 509 40.8
branch 74 342 21.6
condition 11 123 8.9
subroutine 19 35 54.2
pod 1 14 7.1
total 313 1023 30.6


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             package Mail::SpamAssassin::Plugin::DKIM;
126              
127 21     21   166 use Mail::SpamAssassin::Plugin;
  21         290  
  21         628  
128 21     21   122 use Mail::SpamAssassin::Logger;
  21         49  
  21         1155  
129 21     21   359 use Mail::SpamAssassin::Timeout;
  21         2205  
  21         587  
130              
131 21     21   556 use strict;
  21         41  
  21         629  
132 21     21   156 use warnings;
  21         44  
  21         677  
133             # use bytes;
134 21     21   119 use re 'taint';
  21         190  
  21         125367  
135              
136             our @ISA = qw(Mail::SpamAssassin::Plugin);
137              
138             # constructor: register the eval rule
139             sub new {
140 62     62 1 254 my $class = shift;
141 62         195 my $mailsaobject = shift;
142              
143 62   33     495 $class = ref($class) || $class;
144 62         424 my $self = $class->SUPER::new($mailsaobject);
145 62         235 bless ($self, $class);
146              
147             # signatures
148 62         373 $self->register_eval_rule("check_dkim_signed");
149 62         221 $self->register_eval_rule("check_dkim_valid");
150 62         257 $self->register_eval_rule("check_dkim_valid_author_sig");
151 62         251 $self->register_eval_rule("check_dkim_testing");
152 62         231 $self->register_eval_rule("check_dkim_valid_envelopefrom");
153              
154             # author domain signing practices
155 62         253 $self->register_eval_rule("check_dkim_adsp");
156 62         248 $self->register_eval_rule("check_dkim_dependable");
157              
158             # whitelisting
159 62         230 $self->register_eval_rule("check_for_dkim_whitelist_from");
160 62         196 $self->register_eval_rule("check_for_def_dkim_whitelist_from");
161              
162             # old names (aliases) for compatibility
163 62         240 $self->register_eval_rule("check_dkim_verified"); # = check_dkim_valid
164 62         226 $self->register_eval_rule("check_dkim_signall"); # = check_dkim_adsp('A')
165 62         223 $self->register_eval_rule("check_dkim_signsome"); # redundant, always false
166              
167 62         334 $self->set_config($mailsaobject->{conf});
168              
169 62         646 return $self;
170             }
171              
172             ###########################################################################
173              
174             sub set_config {
175 62     62 0 229 my($self, $conf) = @_;
176 62         146 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 62         665 });
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 62         560 });
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 62         510 });
456              
457             push (@cmds, {
458             setting => 'adsp_override',
459             type => $Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE,
460             code => sub {
461 300     300   1055 my ($self, $key, $value, $line) = @_;
462 300         932 local ($1,$2);
463 300 50 33     1997 unless (defined $value && $value !~ /^$/) {
464 0         0 return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
465             }
466 300 50       1781 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 300         1037 my $domain = lc $1; # author's domain
472 300         772 my $adsp = $2; # author domain signing practices
473 300 100       874 $adsp = 'discardable' if !defined $adsp;
474 300         736 $adsp = lc $adsp;
475 300 50       1316 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 300         1125 else { $adsp = uc substr($adsp,0,1) } # N/U/A/D/1/2/3
479 300         2571 $self->{parser}->{conf}->{adsp_override}->{$domain} = $adsp;
480             }
481 62         494 });
482              
483             # minimal signing key size in bits that is acceptable for whitelisting
484 62         337 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 62         312 push (@cmds, {
508             setting => 'dkim_timeout',
509             is_admin => 1,
510             default => 5,
511             type => $Mail::SpamAssassin::Conf::CONF_TYPE_DURATION
512             });
513              
514 62         304 $conf->{parser}->register_commands(\@cmds);
515             }
516              
517             # ---------------------------------------------------------------------------
518              
519             sub check_dkim_signed {
520 81     81 0 275 my ($self, $pms, $full_ref, @acceptable_domains) = @_;
521 81 50       286 $self->_check_dkim_signature($pms) if !$pms->{dkim_checked_signature};
522 81         167 my $result = 0;
523 81 50       264 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 81         1380 return $result;
531             }
532              
533             sub check_dkim_valid {
534 81     81 0 251 my ($self, $pms, $full_ref, @acceptable_domains) = @_;
535 81 50       271 $self->_check_dkim_signature($pms) if !$pms->{dkim_checked_signature};
536 81         165 my $result = 0;
537 81 50       286 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 81         1298 return $result;
546             }
547              
548             sub check_dkim_valid_author_sig {
549 81     81 0 277 my ($self, $pms, $full_ref, @acceptable_domains) = @_;
550 81 50       277 $self->_check_dkim_signature($pms) if !$pms->{dkim_checked_signature};
551 81         196 my $result = 0;
552 81 50       163 if (!%{$pms->{dkim_has_valid_author_sig}}) {
  81         319  
553             # don't bother
554             } else {
555 0         0 $result = $self->_check_dkim_signed_by($pms,1,1,\@acceptable_domains);
556             }
557 81         1317 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 486     486 0 1819 my ($self, $pms, $adsp_char, @domains_list) = @_;
589 486 100       1691 $self->_check_dkim_signature($pms) if !$pms->{dkim_checked_signature};
590 486         781 my $result = 0;
591 486 100       1253 if (!$pms->{dkim_signatures_ready}) {
592             # don't bother
593             } else {
594 24 100       66 $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 24 50       94 $adsp_char = 'NAD123' if $adsp_char eq '*'; # a shorthand for NAD123
599              
600 24 100       46 if ( !(grep { index($adsp_char,$_) >= 0 } values %{$pms->{dkim_adsp}}) ) {
  24 50       146  
  24         72  
601             # not the right ADSP type
602             } elsif (!@domains_list) {
603 4         8 $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 486         8160 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 8     8   20 my ($self) = @_;
667              
668 8 100       39 if (!$self->{tried_loading}) {
669 1         5 $self->{service_available} = 0;
670             my $timemethod = $self->{main}->UNIVERSAL::can("time_method") &&
671 1   33     14 $self->{main}->time_method("dkim_load_modules");
672 1         2 my $eval_stat;
673             eval {
674             # Have to do this so that RPM doesn't find these as required perl modules.
675 1         3 { require Mail::DKIM::Verifier }
  1         588  
676 1 50       2 } or do {
677 0 0       0 $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  0         0  
678             };
679 1         46407 $self->{tried_loading} = 1;
680              
681 1 50       8 if (defined $eval_stat) {
682 0         0 dbg("dkim: cannot load Mail::DKIM module, DKIM checks disabled: %s",
683             $eval_stat);
684             } else {
685 1         26 my $version = Mail::DKIM::Verifier->VERSION;
686 1 50       9 if ($version >= 0.31) {
687 1         7 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 1         5 $self->{service_available} = 1;
693              
694             my $adsp_avail =
695 1         4 eval { require Mail::DKIM::AuthorDomainPolicy }; # since 0.34
  1         7  
696 1 50       6 if (!$adsp_avail) { # fallback to pre-ADSP policy
697 0         0 eval { require Mail::DKIM::DkimPolicy } # ignoring status
  0         0  
698             }
699             }
700             }
701 8         34 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 81     81   197 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 81         168 my %author_domains; local $1;
  81         234  
754 81         350 my @authors = grep { defined $_ } ( $pms->get('from:addr',undef) );
  81         467  
755 81         290 for (@authors) {
756             # be tolerant, ignore trailing WSP after a domain name
757 42 50       595 $author_domains{lc $1} = 1 if /\@([^\@]+?)[ \t]*\z/s;
758             }
759 81         317 $pms->{dkim_author_addresses} = \@authors; # list of full addresses
760 81         338 $pms->{dkim_author_domains} = \%author_domains; # hash of their domains
761             }
762              
763             sub _check_dkim_signature {
764 81     81   217 my ($self, $pms) = @_;
765              
766 81         211 my $conf = $pms->{conf};
767 81         155 my($verifier, @signatures, @valid_signatures);
768              
769 81         222 $pms->{dkim_checked_signature} = 1; # has this sub already been invoked?
770 81         193 $pms->{dkim_signatures_ready} = 0; # have we obtained & verified signatures?
771 81         246 $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 81         243 $pms->{dkim_signatures} = \@signatures;
777 81         246 $pms->{dkim_valid_signatures} = \@valid_signatures;
778 81         186 $pms->{dkim_signed} = 0;
779 81         228 $pms->{dkim_valid} = 0;
780 81         199 $pms->{dkim_key_testing} = 0;
781             # the following hashes are keyed by a signing domain (SDID):
782 81         268 $pms->{dkim_author_sig_tempfailed} = {}; # DNS timeout verifying author sign.
783 81         282 $pms->{dkim_has_valid_author_sig} = {}; # a valid author domain signature
784 81         252 $pms->{dkim_has_any_author_sig} = {}; # valid or invalid author domain sign.
785              
786 81 50       500 $self->_get_authors($pms) if !$pms->{dkim_author_addresses};
787              
788 81         236 my $suppl_attrib = $pms->{msg}->{suppl_attrib};
789 81 50 66     470 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 81 50       504 if ($pms->{dkim_signatures_ready}) {
    100          
    50          
800             # signatures already available and verified
801             } elsif (!$pms->is_dns_available()) {
802 77         329 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 4   33     54 $self->{main}->time_method("check_dkim_signature");
809 4 50       109 if (Mail::DKIM::Verifier->VERSION >= 0.40) {
810 4         16 my $edns = $conf->{dns_options}->{edns};
811 4 50 33     24 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 4         10 my $res = $self->{main}->{resolver};
817 4         23 dbg("dkim: providing our own resolver: %s", ref $res);
818 4         28 Mail::DKIM::DNS::resolver($res);
819             }
820             }
821 4         114 $verifier = Mail::DKIM::Verifier->new;
822 4 50       306 if (!$verifier) {
823 0         0 dbg("dkim: cannot create Mail::DKIM::Verifier object");
824 0         0 return;
825             }
826 4         17 $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 4         22 my $str = $pms->{msg}->get_pristine();
834 4 50       22 if ($pms->{msg}->{line_ending} eq "\015\012") {
835             # message already CRLF, just feed it
836 0         0 $verifier->PRINT($str);
837             } else {
838             # feeding large chunk to Mail::DKIM is _much_ faster than line-by-line
839 4         11 my $str2 = $str; # make a copy, sigh
840 4         40 $str2 =~ s/\012/\015\012/gs; # LF -> CRLF
841 4         29 $verifier->PRINT($str2);
842 4         1270 undef $str2;
843             }
844 4         20 1;
845 4 50       9 } or do { # intercept die() exceptions and render safe
846 0 0       0 my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  0         0  
847 0         0 dbg("dkim: verification failed, intercepted error: $eval_stat");
848 0         0 return 0; # cannot verify message
849             };
850              
851 4         13 my $timeout = $conf->{dkim_timeout};
852             my $timer = Mail::SpamAssassin::Timeout->new(
853 4         34 { secs => $timeout, deadline => $pms->{master_deadline} });
854              
855             my $err = $timer->run_and_catch(sub {
856 4     4   19 dbg("dkim: performing public key lookup and signature verification");
857 4         26 $verifier->CLOSE(); # the action happens here
858              
859             # currently SpamAssassin's parsing is better than Mail::Address parsing,
860             # don't bother fetching $verifier->message_originator->address
861             # to replace what we already have in $pms->{dkim_author_addresses}
862              
863             # versions before 0.29 only provided a public interface to fetch one
864             # signature, newer versions allow access to all signatures of a message
865 4 50       191 @signatures = $verifier->UNIVERSAL::can("signatures") ?
866             $verifier->signatures : $verifier->signature;
867 4         42 });
868 4 50       28 if ($timer->timed_out()) {
    50          
869 0         0 dbg("dkim: public key lookup or verification timed out after %s s",
870             $timeout );
871             #***
872             # $pms->{dkim_author_sig_tempfailed}->{$_} = 1 for ...
873              
874             } elsif ($err) {
875 0         0 chomp $err;
876 0         0 dbg("dkim: public key lookup or verification failed: $err");
877             }
878 4         26 $pms->{dkim_signatures_ready} = 1;
879 4 50 33     22 if (!@signatures || !$pms->{tests_already_hit}->{'__TRUNCATED'}) {
880 4         21 $pms->{dkim_signatures_dependable} = 1;
881             }
882             }
883              
884 81 100       430 if ($pms->{dkim_signatures_ready}) {
885 4         9 my $sig_result_supported;
886 4         9 my $minimum_key_bits = $conf->{dkim_minimum_key_bits};
887 4         13 foreach my $signature (@signatures) {
888             # old versions of Mail::DKIM would give undef for an invalid signature
889 0 0       0 next if !defined $signature;
890 0 0       0 next if !$signature->selector; # empty selector
891              
892 0         0 $sig_result_supported = $signature->UNIVERSAL::can("result_detail");
893 0         0 my($info, $valid, $expired);
894 0 0       0 $valid =
895             ($sig_result_supported ? $signature : $verifier)->result eq 'pass';
896 0 0       0 $info = $valid ? 'VALID' : 'FAILED';
897 0 0 0     0 if ($valid && $signature->UNIVERSAL::can("check_expiration")) {
898 0         0 $expired = !$signature->check_expiration;
899 0 0       0 $info .= ' EXPIRED' if $expired;
900             }
901 0         0 my $key_size;
902 0 0 0     0 if ($valid && !$expired && $minimum_key_bits) {
      0        
903 0         0 $key_size = eval { my $pk = $signature->get_public_key;
  0         0  
904 0 0 0     0 $pk && $pk->cork && $pk->cork->size * 8 };
905 0 0       0 if ($key_size) {
906 0         0 $signature->{_spamassassin_key_size} = $key_size; # stash it for later
907 0 0       0 $info .= " WEAK($key_size)" if $key_size < $minimum_key_bits;
908             }
909             }
910 0 0 0     0 push(@valid_signatures, $signature) if $valid && !$expired;
911              
912             # check if we have a potential Author Domain Signature, valid or not
913 0         0 my $d = $signature->domain;
914 0 0       0 if (!defined $d) {
915             # can be undefined on a broken signature with missing required tags
916             } else {
917 0         0 $d = lc $d;
918 0 0       0 if ($pms->{dkim_author_domains}->{$d}) { # SDID matches author domain
919 0         0 $pms->{dkim_has_any_author_sig}->{$d} = 1;
920 0 0 0     0 if ($valid && !$expired &&
    0 0        
    0 0        
921             $key_size && $key_size >= $minimum_key_bits) {
922 0         0 $pms->{dkim_has_valid_author_sig}->{$d} = 1;
923             } elsif ( ($sig_result_supported ? $signature
924             : $verifier)->result_detail
925             =~ /\b(?:timed out|SERVFAIL)\b/i) {
926 0         0 $pms->{dkim_author_sig_tempfailed}->{$d} = 1;
927             }
928             }
929             }
930 0 0       0 if (would_log("dbg","dkim")) {
931             dbg("dkim: %s %s, i=%s, d=%s, s=%s, a=%s, c=%s, %s, %s, %s",
932             $info,
933             $signature->isa('Mail::DKIM::DkSignature') ? 'DK' : 'DKIM',
934             map(!defined $_ ? '(undef)' : $_,
935             $signature->identity, $d, $signature->selector,
936             $signature->algorithm, scalar($signature->canonicalization),
937             $key_size ? "key_bits=$key_size" : "unknown key size",
938             ($sig_result_supported ? $signature : $verifier)->result ),
939 0 0 0     0 defined $d && $pms->{dkim_author_domains}->{$d}
    0          
    0          
    0          
    0          
940             ? 'matches author domain'
941             : 'does not match author domain',
942             );
943             }
944             }
945 4 50       17 if (@valid_signatures) {
    50          
946 0         0 $pms->{dkim_signed} = 1;
947 0         0 $pms->{dkim_valid} = 1;
948             # let the result stand out more clearly in the log, use uppercase
949 0         0 my $sig = $valid_signatures[0];
950 0 0       0 my $sig_res = ($sig_result_supported ? $sig : $verifier)->result_detail;
951 0         0 dbg("dkim: signature verification result: %s", uc($sig_res));
952              
953             # supply values for both tags
954 0         0 my(%seen1, %seen2, %seen3, @identity_list, @domain_list, @selector_list);
955 0   0     0 @identity_list = grep(defined $_ && $_ ne '' && !$seen1{$_}++,
956             map($_->identity, @valid_signatures));
957 0   0     0 @domain_list = grep(defined $_ && $_ ne '' && !$seen2{$_}++,
958             map($_->domain, @valid_signatures));
959 0   0     0 @selector_list = grep(defined $_ && $_ ne '' && !$seen3{$_}++,
960             map($_->selector, @valid_signatures));
961 0 0       0 $pms->set_tag('DKIMIDENTITY',
962             @identity_list == 1 ? $identity_list[0] : \@identity_list);
963 0 0       0 $pms->set_tag('DKIMDOMAIN',
964             @domain_list == 1 ? $domain_list[0] : \@domain_list);
965 0 0       0 $pms->set_tag('DKIMSELECTOR',
966             @selector_list == 1 ? $selector_list[0] : \@selector_list);
967             } elsif (@signatures) {
968 0         0 $pms->{dkim_signed} = 1;
969 0         0 my $sig = $signatures[0];
970 0 0 0     0 my $sig_res =
971             ($sig_result_supported && $sig ? $sig : $verifier)->result_detail;
972 0         0 dbg("dkim: signature verification result: %s", uc($sig_res));
973             } else {
974 4         14 dbg("dkim: signature verification result: none");
975             }
976             }
977             }
978              
979             sub _check_dkim_adsp {
980 4     4   12 my ($self, $pms) = @_;
981              
982 4         11 $pms->{dkim_checked_adsp} = 1;
983              
984             # a message may have multiple authors (RFC 5322),
985             # and hence multiple signing policies (RFC 5617)
986 4         13 $pms->{dkim_adsp} = {}; # a hash: author_domain => adsp
987 4         11 my $practices_as_string = '';
988              
989 4 50       15 $self->_get_authors($pms) if !$pms->{dkim_author_addresses};
990              
991             # collect only fully qualified domain names, allow '-', think of IDN
992 4         38 my @author_domains = grep { /.\.[a-z-]{2,}\z/si }
993 4         8 keys %{$pms->{dkim_author_domains}};
  4         25  
994              
995 4         44 my %label =
996             ('D' => 'discardable', 'A' => 'all', 'U' => 'unknown', 'N' => 'nxdomain',
997             '1' => 'custom_low', '2' => 'custom_med', '3' => 'custom_high');
998              
999             # must check the message first to obtain signer, domain, and verif. status
1000 4 50       18 $self->_check_dkim_signature($pms) if !$pms->{dkim_checked_signature};
1001              
1002 4 50       38 if (!$pms->{dkim_signatures_ready}) {
    50          
1003 0         0 dbg("dkim: adsp not retrieved, signatures not obtained");
1004              
1005             } elsif (!@author_domains) {
1006 0         0 dbg("dkim: adsp not retrieved, no author f.q. domain name");
1007 0         0 $practices_as_string = 'no author domains, ignored';
1008              
1009             } else {
1010              
1011 4         13 foreach my $author_domain (@author_domains) {
1012 4         9 my $adsp;
1013              
1014 4 50 33     33 if ($pms->{dkim_has_valid_author_sig}->{$author_domain}) {
    50          
    50          
1015             # don't fetch adsp when valid
1016             # RFC 5617: If a message has an Author Domain Signature, ADSP provides
1017             # no benefit relative to that domain since the message is already known
1018             # to be compliant with any possible ADSP for that domain. [...]
1019             # implementations SHOULD avoid doing unnecessary DNS lookups
1020             #
1021 0         0 dbg("dkim: adsp not retrieved, author domain signature is valid");
1022 0         0 $practices_as_string = 'valid a. d. signature';
1023              
1024             } elsif ($pms->{dkim_author_sig_tempfailed}->{$author_domain}) {
1025 0         0 dbg("dkim: adsp ignored, tempfail varifying author domain signature");
1026 0         0 $practices_as_string = 'pub key tempfailed, ignored';
1027              
1028             } elsif ($pms->{dkim_has_any_author_sig}->{$author_domain} &&
1029             !$pms->{dkim_signatures_dependable}) {
1030             # the message did have an Author Domain Signature but it wasn't valid;
1031             # we also believe the message was truncated just before being passed
1032             # to SpamAssassin, which is a likely reason for verification failure,
1033             # so we shouldn't take it too harsh with ADSP rules - just pretend
1034             # the ADSP was 'unknown'
1035             #
1036 0         0 dbg("dkim: adsp ignored, message was truncated, ".
1037             "invalid author domain signature");
1038 0         0 $practices_as_string = 'truncated, ignored';
1039              
1040             } else {
1041             # search the adsp_override list
1042              
1043             # for a domain a.b.c.d it searches the hash in the following order:
1044             # a.b.c.d
1045             # *.b.c.d
1046             # *.c.d
1047             # *.d
1048             # *
1049 4         9 my $matched_key;
1050 4         12 my $p = $pms->{conf}->{adsp_override};
1051 4 50       15 if ($p) {
1052 4         17 my @d = split(/\./, $author_domain);
1053 4         18 @d = map { shift @d; join('.', '*', @d) } (0..$#d);
  10         15  
  10         34  
1054 4         22 for my $key ($author_domain, @d) {
1055 14         28 $adsp = $p->{$key};
1056 14 50       34 if (defined $adsp) { $matched_key = $key; last }
  0         0  
  0         0  
1057             }
1058             }
1059              
1060 4 50       36 if (defined $adsp) {
    50          
    50          
1061 0         0 dbg("dkim: adsp override for domain %s", $author_domain);
1062 0         0 $practices_as_string = 'override';
1063 0 0       0 $practices_as_string .=
1064             " by $matched_key" if $matched_key ne $author_domain;
1065              
1066             } elsif (!$pms->is_dns_available()) {
1067 0         0 dbg("dkim: adsp not retrieved, DNS resolving not available");
1068              
1069             } elsif (!$self->_dkim_load_modules()) {
1070 0         0 dbg("dkim: adsp not retrieved, module Mail::DKIM not available");
1071              
1072             } else { # do the ADSP DNS lookup
1073             my $timemethod = $self->{main}->UNIVERSAL::can("time_method") &&
1074 4   33     53 $self->{main}->time_method("check_dkim_adsp");
1075              
1076 4         11 my $practices; # author domain signing practices object
1077 4         9 my $timeout = $pms->{conf}->{dkim_timeout};
1078             my $timer = Mail::SpamAssassin::Timeout->new(
1079 4         27 { secs => $timeout, deadline => $pms->{master_deadline} });
1080             my $err = $timer->run_and_catch(sub {
1081             eval {
1082 4 50       53 if (Mail::DKIM::AuthorDomainPolicy->UNIVERSAL::can("fetch")) {
1083 4         19 dbg("dkim: adsp: performing lookup on _adsp._domainkey.%s",
1084             $author_domain);
1085             # get our Net::DNS::Resolver object
1086 4         27 my $res = $self->{main}->{resolver}->get_resolver;
1087 4         28 $practices = Mail::DKIM::AuthorDomainPolicy->fetch(
1088             Protocol => "dns", Domain => $author_domain,
1089             DnsResolver => $res);
1090             }
1091 4         758 1;
1092 4 50   4   24 } or do {
1093             # fetching/parsing adsp record may throw error, ignore such s.p.
1094 0 0       0 my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  0         0  
1095 0         0 dbg("dkim: adsp: fetch or parse on domain %s failed: %s",
1096             $author_domain, $eval_stat);
1097 0         0 undef $practices;
1098             };
1099 4         43 });
1100 4 50       47 if ($timer->timed_out()) {
    50          
1101 0         0 dbg("dkim: adsp lookup on domain %s timed out after %s seconds",
1102             $author_domain, $timeout);
1103             } elsif ($err) {
1104 0         0 chomp $err;
1105 0         0 dbg("dkim: adsp lookup on domain %s failed: %s",
1106             $author_domain, $err);
1107             } else {
1108 4         9 my $sp; # ADSP: unknown / all / discardable
1109 4 50       26 ($sp) = $practices->policy if $practices;
1110 4 50 33     79 if (!defined $sp || $sp eq '') { # SERVFAIL or a timeout
1111 0         0 dbg("dkim: signing practices on %s unavailable", $author_domain);
1112 0         0 $adsp = 'U';
1113 0         0 $practices_as_string = 'dns: no result';
1114             } else {
1115 4 50       42 $adsp = $sp eq "unknown" ? 'U' # most common
    50          
    50          
    50          
    50          
1116             : $sp eq "all" ? 'A'
1117             : $sp eq "discardable" ? 'D' # ADSP
1118             : $sp eq "strict" ? 'D' # old style SSP
1119             : uc($sp) eq "NXDOMAIN" ? 'N'
1120             : 'U';
1121 4         24 $practices_as_string = 'dns: ' . $sp;
1122             }
1123             }
1124             }
1125             }
1126              
1127             # is signing practices available?
1128 4 50       27 $pms->{dkim_adsp}->{$author_domain} = $adsp if defined $adsp;
1129              
1130             dbg("dkim: adsp result: %s (%s), author domain '%s'",
1131 4 50       34 !defined($adsp) ? '-' : $adsp.'/'.$label{$adsp},
1132             $practices_as_string, $author_domain);
1133             }
1134             }
1135             }
1136              
1137             sub _check_dkim_whitelist {
1138 0     0     my ($self, $pms) = @_;
1139              
1140 0           $pms->{whitelist_checked} = 1;
1141              
1142 0 0         $self->_get_authors($pms) if !$pms->{dkim_author_addresses};
1143              
1144 0           my $authors_str = join(", ", @{$pms->{dkim_author_addresses}});
  0            
1145 0 0         if ($authors_str eq '') {
1146 0           dbg("dkim: check_dkim_whitelist: could not find author address");
1147 0           return;
1148             }
1149              
1150             # collect whitelist entries matching the author from all lists
1151 0           my @acceptable_sdid_tuples;
1152 0           $self->_wlcheck_acceptable_signature($pms, \@acceptable_sdid_tuples,
1153             'def_whitelist_from_dkim');
1154 0           $self->_wlcheck_author_signature($pms, \@acceptable_sdid_tuples,
1155             'def_whitelist_auth');
1156 0           $self->_wlcheck_acceptable_signature($pms, \@acceptable_sdid_tuples,
1157             'whitelist_from_dkim');
1158 0           $self->_wlcheck_author_signature($pms, \@acceptable_sdid_tuples,
1159             'whitelist_auth');
1160 0 0         if (!@acceptable_sdid_tuples) {
1161 0           dbg("dkim: no wl entries match author %s, no need to verify sigs",
1162             $authors_str);
1163 0           return;
1164             }
1165              
1166             # if the message doesn't pass DKIM validation, it can't pass DKIM whitelist
1167              
1168             # trigger a DKIM check;
1169             # continue if one or more signatures are valid or we want the debug info
1170 0 0 0       return unless $self->check_dkim_valid($pms) || would_log("dbg","dkim");
1171 0 0         return unless $pms->{dkim_signatures_ready};
1172              
1173             # now do all the matching in one go, against all signatures in a message
1174 0           my($any_match_at_all, $any_match_by_wl_ref) =
1175             _wlcheck_list($self, $pms, \@acceptable_sdid_tuples);
1176              
1177 0           my(@valid,@fail);
1178 0           foreach my $wl (keys %$any_match_by_wl_ref) {
1179 0           my $match = $any_match_by_wl_ref->{$wl};
1180 0 0         if (defined $match) {
1181 0 0         $pms->{"dkim_match_in_$wl"} = 1 if $match;
1182 0 0         push(@{$match ? \@valid : \@fail}, "$wl/$match");
  0            
1183             }
1184             }
1185 0 0         if (@valid) {
    0          
1186 0           dbg("dkim: author %s, WHITELISTED by %s",
1187             $authors_str, join(", ",@valid));
1188             } elsif (@fail) {
1189 0           dbg("dkim: author %s, found in %s BUT IGNORED",
1190             $authors_str, join(", ",@fail));
1191             } else {
1192 0           dbg("dkim: author %s, not in any dkim whitelist", $authors_str);
1193             }
1194             }
1195              
1196             # check for verifier-acceptable signatures; an empty (or undefined) signing
1197             # domain in a whitelist implies checking for an Author Domain Signature
1198             #
1199             sub _wlcheck_acceptable_signature {
1200 0     0     my ($self, $pms, $acceptable_sdid_tuples_ref, $wl) = @_;
1201 0           my $wl_ref = $pms->{conf}->{$wl};
1202 0           foreach my $author (@{$pms->{dkim_author_addresses}}) {
  0            
1203 0           foreach my $white_addr (keys %$wl_ref) {
1204 0           my $wl_addr_ref = $wl_ref->{$white_addr};
1205 0           my $re = qr/$wl_addr_ref->{re}/i;
1206             # dbg("dkim: WL %s %s, d: %s", $wl, $white_addr,
1207             # join(", ", map { $_ eq '' ? "''" : $_ } @{$wl_addr_ref->{domain}}));
1208 0 0         if ($author =~ $re) {
1209 0           foreach my $sdid (@{$wl_addr_ref->{domain}}) {
  0            
1210 0           push(@$acceptable_sdid_tuples_ref, [$author,$sdid,$wl,$re]);
1211             }
1212             }
1213             }
1214             }
1215             }
1216              
1217             # use a traditional whitelist_from -style addrlist, the only acceptable DKIM
1218             # signature is an Author Domain Signature. Note: don't pre-parse and store
1219             # domains; that's inefficient memory-wise and only saves one m//
1220             #
1221             sub _wlcheck_author_signature {
1222 0     0     my ($self, $pms, $acceptable_sdid_tuples_ref, $wl) = @_;
1223 0           my $wl_ref = $pms->{conf}->{$wl};
1224 0           foreach my $author (@{$pms->{dkim_author_addresses}}) {
  0            
1225 0           foreach my $white_addr (keys %$wl_ref) {
1226 0           my $re = qr/$wl_ref->{$white_addr}/i;
1227             # dbg("dkim: WL %s %s", $wl, $white_addr);
1228 0 0         if ($author =~ $re) {
1229 0           push(@$acceptable_sdid_tuples_ref, [$author,undef,$wl,$re]);
1230             }
1231             }
1232             }
1233             }
1234              
1235             sub _wlcheck_list {
1236 0     0     my ($self, $pms, $acceptable_sdid_tuples_ref) = @_;
1237              
1238 0           my %any_match_by_wl;
1239 0           my $any_match_at_all = 0;
1240 0           my $verifier = $pms->{dkim_verifier};
1241 0           my $minimum_key_bits = $pms->{conf}->{dkim_minimum_key_bits};
1242              
1243             # walk through all signatures present in a message
1244 0           foreach my $signature (@{$pms->{dkim_signatures}}) {
  0            
1245             # old versions of Mail::DKIM would give undef for an invalid signature
1246 0 0         next if !defined $signature;
1247 0 0         next if !$signature->selector; # empty selector
1248              
1249 0           my $sig_result_supported = $signature->UNIVERSAL::can("result_detail");
1250 0           my($info, $valid, $expired, $key_size_weak);
1251 0 0         $valid =
1252             ($sig_result_supported ? $signature : $verifier)->result eq 'pass';
1253 0 0         $info = $valid ? 'VALID' : 'FAILED';
1254 0 0 0       if ($valid && $signature->UNIVERSAL::can("check_expiration")) {
1255 0           $expired = !$signature->check_expiration;
1256 0 0         $info .= ' EXPIRED' if $expired;
1257             }
1258 0 0 0       if ($valid && !$expired && $minimum_key_bits) {
      0        
1259 0           my $key_size = $signature->{_spamassassin_key_size};
1260 0 0 0       if ($key_size && $key_size < $minimum_key_bits) {
1261 0           $info .= " WEAK($key_size)"; $key_size_weak = 1;
  0            
1262             }
1263             }
1264              
1265 0           my $sdid = $signature->domain;
1266 0 0         $sdid = lc $sdid if defined $sdid;
1267              
1268 0           my %tried_authors;
1269 0           foreach my $entry (@$acceptable_sdid_tuples_ref) {
1270 0           my($author, $acceptable_sdid, $wl, $re) = @$entry;
1271             # $re and $wl are here for logging purposes only, $re already checked.
1272             # The $acceptable_sdid is a verifier-acceptable signing domain
1273             # identifier (to be matched against a 'd' tag in signatures).
1274             # When $acceptable_sdid is undef or an empty string it implies
1275             # a check for Author Domain Signature.
1276              
1277 0           local $1;
1278 0 0         my $author_domain = $author !~ /\@([^\@]+)\z/s ? '' : lc $1;
1279 0           $tried_authors{$author} = 1; # for logging purposes
1280              
1281 0           my $matches = 0;
1282 0 0 0       if (!defined $sdid) {
    0          
1283             # don't bother, invalid signature with a missing 'd' tag
1284              
1285             } elsif (!defined $acceptable_sdid || $acceptable_sdid eq '') {
1286             # An "Author Domain Signature" (sometimes called a first-party
1287             # signature) is a Valid Signature in which the domain name of the
1288             # DKIM signing entity, i.e., the d= tag in the DKIM-Signature header
1289             # field, is the same as the domain name in the Author Address.
1290             # Following [RFC5321], domain name comparisons are case insensitive.
1291              
1292             # checking for Author Domain Signature
1293 0 0         $matches = 1 if $sdid eq $author_domain;
1294              
1295             } else { # checking for verifier-acceptable signature
1296             # The second argument to a 'whitelist_from_dkim' option is now (since
1297             # version 3.3.0) supposed to be a signing domain (SDID), no longer an
1298             # identity (AUID). Nevertheless, be prepared to accept the full e-mail
1299             # address there for compatibility, and just ignore its local-part.
1300              
1301 0 0         $acceptable_sdid = $1 if $acceptable_sdid =~ /\@([^\@]*)\z/s;
1302 0 0         if ($acceptable_sdid =~ s/^\*?\.//s) {
1303 0 0         $matches = 1 if $sdid =~ /\.\Q$acceptable_sdid\E\z/si;
1304             } else {
1305 0 0         $matches = 1 if $sdid eq lc $acceptable_sdid;
1306             }
1307             }
1308 0 0         if ($matches) {
1309 0 0         if (would_log("dbg","dkim")) {
1310 0 0         if ($sdid eq $author_domain) {
1311 0           dbg("dkim: %s author domain signature by %s, MATCHES %s %s",
1312             $info, $sdid, $wl, $re);
1313             } else {
1314 0           dbg("dkim: %s third-party signature by %s, author domain %s, ".
1315             "MATCHES %s %s", $info, $sdid, $author_domain, $wl, $re);
1316             }
1317             }
1318             # a defined value indicates at least a match, not necessarily valid
1319             # (this complication servers to preserve logging compatibility)
1320 0 0         $any_match_by_wl{$wl} = '' if !exists $any_match_by_wl{$wl};
1321             }
1322             # only valid signature can cause whitelisting
1323 0 0 0       $matches = 0 if !$valid || $expired || $key_size_weak;
      0        
1324              
1325 0 0         if ($matches) {
1326 0           $any_match_at_all = 1;
1327 0           $any_match_by_wl{$wl} = $sdid; # value used for debug logging
1328             }
1329             }
1330 0 0         dbg("dkim: %s signature by %s, author %s, no valid matches",
    0          
1331             $info, defined $sdid ? $sdid : '(undef)',
1332             join(", ", keys %tried_authors)) if !$any_match_at_all;
1333             }
1334 0           return ($any_match_at_all, \%any_match_by_wl);
1335             }
1336              
1337             1;