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