File Coverage

blib/lib/Mail/Milter/Authentication/Handler/DKIM.pm
Criterion Covered Total %
statement 205 271 75.6
branch 65 118 55.0
condition 27 57 47.3
subroutine 19 20 95.0
pod 1 10 10.0
total 317 476 66.6


line stmt bran cond sub pod time code
1             package Mail::Milter::Authentication::Handler::DKIM;
2 39     39   24508 use 5.20.0;
  39         197  
3 39     39   279 use strict;
  39         128  
  39         975  
4 39     39   231 use warnings;
  39         109  
  39         1338  
5 39     39   253 use Mail::Milter::Authentication::Pragmas;
  39         120  
  39         323  
6             # ABSTRACT: Handler class for DKIM
7             our $VERSION = '3.20230911'; # VERSION
8 39     39   9584 use base 'Mail::Milter::Authentication::Handler';
  39         155  
  39         4320  
9 39     39   17861 use Mail::DKIM 1.20200824;
  39         4843  
  39         1139  
10 39     39   15797 use Mail::DKIM::DNS;
  39         34689  
  39         1286  
11 39     39   15382 use Mail::DKIM::KeyValueList;
  39         48488  
  39         1461  
12 39     39   20412 use Mail::DKIM::Verifier;
  39         2006711  
  39         144663  
13              
14             sub default_config {
15             return {
16 1     1 0 2064 'hide_none' => 0,
17             'hide_domainkeys' => 0,
18             'check_adsp' => 1,
19             'show_default_adsp' => 0,
20             'adsp_hide_none' => 0,
21             'extra_properties' => 0,
22             'no_strict' => 0,
23             };
24             }
25              
26             sub grafana_rows {
27 1     1 0 4513 my ( $self ) = @_;
28 1         2 my @rows;
29 1         9 push @rows, $self->get_json( 'DKIM_metrics' );
30 1         6 return \@rows;
31             }
32              
33             sub register_metrics {
34             return {
35 43     43 1 514 'dkim_total' => 'The number of emails processed for DKIM',
36             'dkim_signatures' => 'The number of signatures processed for DKIM',
37             };
38             }
39              
40             sub envfrom_callback {
41 205     205 0 845 my ( $self, $env_from ) = @_;
42 205         846 $self->{'failmode'} = 0;
43 205         879 $self->{'headers'} = [];
44 205         779 $self->{'has_dkim'} = 0;
45 205         837 $self->{'valid_domains'} = {};
46 205         972 $self->{'carry'} = q{};
47 205         1522 $self->destroy_object('dkim');
48             }
49              
50             sub show_domainkeys {
51 36     36 0 162 my ( $self ) = @_;
52 36         167 my $config = $self->handler_config();
53 36 50       375 return 1 if ! exists $config->{'hide_domainkeys'};
54 0 0       0 return 0 if $config->{'hide_domainkeys'};
55 0         0 return 1;
56             }
57              
58             sub header_callback {
59 1431     1431 0 4270 my ( $self, $header, $value, $original ) = @_;
60 1431 50       4190 return if ( $self->{'failmode'} );
61 1431         2723 my $EOL = "\015\012";
62 1431         3911 my $dkim_chunk = $original . $EOL;
63 1431         12938 $dkim_chunk =~ s/\015?\012/$EOL/g;
64 1431         3333 push @{$self->{'headers'}} , $dkim_chunk;
  1431         4384  
65              
66 1431 100       4709 if ( lc($header) eq 'dkim-signature' ) {
67 163         656 $self->{'has_dkim'} = 1;
68              
69 163         727 my $parsed = eval{ Mail::DKIM::KeyValueList->parse( $value ) };
  163         2160  
70 163         41551 $self->handle_exception( $@ );
71 163 50       877 if ( $parsed ) {
72 163         889 my $domain = $parsed->get_tag('d');
73 163         2103 my $selector = $parsed->get_tag('s');
74 163 50 33     2238 if ( defined $selector && defined $domain ) {
75 163         887 my $resolver = $self->get_object('resolver');
76 163         889 my $lookup = $selector.'._domainkey.'.$domain;
77 163         340 eval{ $resolver->bgsend( $lookup, 'TXT' ) };
  163         1898  
78 163         287177 $self->handle_exception( $@ );
79 163         1252 $self->dbgout( 'DNSEarlyLookup', "$lookup TXT", LOG_DEBUG );
80 163         625 $lookup = '_adsp._domainkey.'.$domain;
81 163         398 eval{ $resolver->bgsend( $lookup, 'TXT' ) };
  163         722  
82 163         183362 $self->handle_exception( $@ );
83 163         1001 $self->dbgout( 'DNSEarlyLookup', "$lookup TXT", LOG_DEBUG );
84             }
85             }
86             }
87 1431 50       5756 if ( lc($header) eq 'domainkey-signature' ) {
88 0 0       0 $self->{'has_dkim'} = 1 if $self->show_domainkeys();
89             }
90             }
91              
92             sub eoh_callback {
93 205     205 0 831 my ($self) = @_;
94 205 50       912 return if ( $self->{'failmode'} );
95 205         1126 my $config = $self->handler_config();
96              
97 205 100       1206 if ( $self->{'has_dkim'} == 0 ) {
98 74         682 $self->metric_count( 'dkim_total', { 'result' => 'none' } );
99 74         652 $self->dbgout( 'DKIMResult', 'No DKIM headers', LOG_DEBUG );
100 74 100       624 if ( !( $config->{'hide_none'} ) ) {
101 67         790 my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( 'dkim' )->safe_set_value( 'none' );
102 67         5271 $header->add_child( Mail::AuthenticationResults::Header::Comment->new()->safe_set_value( 'no signatures found' ) );
103 67         22869 $self->add_auth_header( $header );
104             }
105 74         413 delete $self->{'headers'};
106             }
107             else {
108              
109 131         355 my $dkim;
110 131         358 eval {
111 131         391 my $UseStrict = 1;
112 131 100       585 if ( $config->{ 'no_strict' } ) {
113 2         6 $UseStrict = 0;
114             }
115 131         2409 $dkim = Mail::DKIM::Verifier->new( 'Strict' => $UseStrict );
116 131         11934 my $resolver = $self->get_object('resolver');
117 131         1020 Mail::DKIM::DNS::resolver($resolver);
118 131         1656 $self->set_object('dkim', $dkim, 1);
119             };
120 131 50       862 if ( my $error = $@ ) {
121 0         0 $self->handle_exception( $error );
122 0         0 $self->log_error( 'DKIM Setup Error ' . $error );
123 0         0 $self->{'failmode'} = 1;
124 0         0 $self->_check_error( $error );
125 0         0 $self->metric_count( 'dkim_total', { 'result' => 'error' } );
126 0         0 delete $self->{'headers'};
127 0         0 return;
128             }
129              
130 131         315 eval {
131             $dkim->PRINT( join q{},
132 131         362 @{ $self->{'headers'} },
  131         1949  
133             "\015\012",
134             );
135             };
136 131 50       343541 if ( my $error = $@ ) {
137 0         0 $self->handle_exception( $error );
138 0         0 $self->log_error( 'DKIM Headers Error ' . $error );
139 0         0 $self->{'failmode'} = 1;
140 0         0 $self->_check_error( $error );
141 0         0 $self->metric_count( 'dkim_total', { 'result' => 'error' } );
142             }
143              
144 131         776 delete $self->{'headers'};
145             }
146              
147 205         1213 $self->{'carry'} = q{};
148             }
149              
150             sub body_callback {
151 203     203 0 761 my ( $self, $body_chunk ) = @_;
152 203 50       984 return if ( $self->{'failmode'} );
153 203 100       969 return if ( $self->{'has_dkim'} == 0 );
154 131         391 my $EOL = "\015\012";
155              
156 131         311 my $dkim_chunk;
157 131 50       580 if ( $self->{'carry'} ne q{} ) {
158 0         0 $dkim_chunk = $self->{'carry'} . $body_chunk;
159 0         0 $self->{'carry'} = q{};
160             }
161             else {
162 131         406 $dkim_chunk = $body_chunk;
163             }
164              
165 131 50       754 if ( substr( $dkim_chunk, -1 ) eq "\015" ) {
166 0         0 $self->{'carry'} = "\015";
167 0         0 $dkim_chunk = substr( $dkim_chunk, 0, -1 );
168             }
169              
170 131         2065 $dkim_chunk =~ s/\015?\012/$EOL/g;
171              
172 131         753 my $dkim = $self->get_object('dkim');
173 131         485 eval {
174 131         816 $dkim->PRINT( $dkim_chunk );
175             };
176 131 50       20614 if ( my $error = $@ ) {
177 0         0 $self->handle_exception( $error );
178 0         0 $self->log_error( 'DKIM Body Error ' . $error );
179 0         0 $self->{'failmode'} = 1;
180 0         0 $self->_check_error( $error );
181 0         0 $self->metric_count( 'dkim_total', { 'result' => 'error' } );
182             }
183             }
184              
185             sub eom_callback {
186 205     205 0 689 my ($self) = @_;
187              
188 205 100       1048 return if ( $self->{'has_dkim'} == 0 );
189 131 50       542 return if ( $self->{'failmode'} );
190              
191 131         565 my $config = $self->handler_config();
192              
193 131         695 my $dkim = $self->get_object('dkim');
194              
195 131         471 eval {
196 131         818 $dkim->PRINT( $self->{'carry'} );
197 131         3172 $dkim->CLOSE();
198 131         390022 $self->check_timeout();
199              
200 131         1363 my $dkim_result = $dkim->result;
201 131         1787 my $dkim_result_detail = $dkim->result_detail;
202              
203 131         2073 $self->metric_count( 'dkim_total', { 'result' => $dkim_result } );
204              
205 131         1263 $self->dbgout( 'DKIMResult', $dkim_result_detail, LOG_DEBUG );
206              
207 131 50       1178 if ( !$dkim->signatures() ) {
208 0 0 0     0 if ( !( $config->{'hide_none'} && $dkim_result eq 'none' ) ) {
209 0         0 my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( 'dkim' )->safe_set_value( $dkim_result );
210 0         0 $header->add_child( Mail::AuthenticationResults::Header::Comment->new()->safe_set_value( 'no signatures found' ) );
211 0         0 $self->add_auth_header( $header );
212             }
213             }
214 131         2055 foreach my $signature ( $dkim->signatures() ) {
215              
216 163         1679 my $otype = ref $signature;
217 163 50       994 my $type =
    50          
218             $otype eq 'Mail::DKIM::DkSignature' ? 'domainkeys'
219             : $otype eq 'Mail::DKIM::Signature' ? 'dkim'
220             : 'dkim';
221 163         746 $self->dbgout( 'DKIMSignatureType', $type, LOG_DEBUG );
222              
223 163         1096 $self->dbgout( 'DKIMSignatureDomain', $signature->domain, LOG_DEBUG );
224 163         976 $self->dbgout( 'DKIMSignatureIdentity', $signature->identity, LOG_DEBUG );
225 163         1375 $self->dbgout( 'DKIMSignatureResult', $signature->result_detail, LOG_DEBUG );
226 163         1127 my $signature_result = $signature->result();
227 163         1736 my $signature_result_detail = $signature->result_detail();
228              
229 163 100       1914 if ( $signature_result eq 'pass' ) {
230 101         500 $self->{'valid_domains'}->{ lc $signature->domain } = 1;
231             }
232              
233 163 100       2571 if ( $signature_result eq 'invalid' ) {
234 24 50       110 if ( $signature_result_detail =~ /DNS query timeout for (.*) at / ) {
235 0         0 my $timeout_domain = $1;
236 0         0 $self->log_error( "TIMEOUT DETECTED: in DKIM result: $timeout_domain" );
237 0         0 $signature_result_detail = "DNS query timeout for $timeout_domain";
238             }
239             }
240              
241 163         515 my $result_comment = q{};
242 163 100 66     1055 if ( $signature_result ne 'pass' and $signature_result ne 'none' ) {
243 62         1130 $signature_result_detail =~ /$signature_result \((.*)\)/;
244 62 50       367 if ( $1 ) {
245 62         246 $result_comment = $1 . ', ';
246             }
247             }
248 163 50 66     948 if (
249             !(
250             $config->{'hide_none'} && $signature_result eq 'none'
251             )
252             )
253             {
254              
255 163   50     469 my $selector = eval{ $signature->selector } // q{};
  163         574  
256 163         2824 my $key = eval {$signature->get_public_key()};
  163         575  
257 163   50     36839 my $key_type = eval{ $key->type() } // 'unknown';
  163         900  
258             # Key size for ed25519 does not make sense
259 163 50 50     2122 my $key_size = $key_type eq 'rsa' ? ( eval{ $key->size() } // 0 ) : 0;
  163         740  
260              
261 163   50     2840 my $hash_algorithm = eval { $signature->hash_algorithm(); } // '';
  163         576  
262 163   50     4140 my $canonicalization = eval { $signature->canonicalization(); } // '';
  163         737  
263              
264 163         4682 my @key_data_parts;
265 163 50       821 push @key_data_parts, $key_size . '-bit' if $key_size;
266 163         528 push @key_data_parts, "$key_type key";
267 163 50       615 push @key_data_parts, $hash_algorithm if $hash_algorithm;
268 163         771 my $key_data = join ' ', @key_data_parts;
269              
270 163         1869 $self->metric_count( 'dkim_signatures', {
271             'type' => $type,
272             'result' => $signature_result,
273             'key_size' => $key_size,
274             'key_type' => $key_type,
275             'hash_algorithm' => $hash_algorithm,
276             'canonicalization' => $canonicalization,
277             } );
278              
279 163 50       1383 if ( $type eq 'domainkeys' ) {
280 0 0       0 if ( $self->show_domainkeys() ) {
281 0         0 my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( $type )->safe_set_value( $signature_result );
282 0         0 $header->add_child( Mail::AuthenticationResults::Header::Comment->new()->safe_set_value( $result_comment . $key_data ) );
283 0         0 $header->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'header.d' )->safe_set_value( $signature->domain() ) );
284 0         0 $header->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'header.b' )->safe_set_value( substr( $signature->data(), 0, 8 ) ) );
285 0 0       0 if ( $config->{'extra_properties'} ) {
286 0 0       0 $header->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'x-bits' )->safe_set_value( $key_size ) ) if $key_size;
287 0         0 $header->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'x-keytype' )->safe_set_value( $key_type ) );
288 0 0       0 $header->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'x-algorithm' )->safe_set_value( $hash_algorithm ) ) if $hash_algorithm;
289 0         0 $header->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'x-selector' )->safe_set_value( $selector ) );
290             }
291 0         0 $self->add_auth_header($header);
292             }
293             }
294             else {
295 163         2263 my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( $type )->safe_set_value( $signature_result );
296 163         14996 $header->add_child( Mail::AuthenticationResults::Header::Comment->new()->safe_set_value( $result_comment . $key_data ) );
297 163         89896 $header->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'header.d' )->safe_set_value( $signature->domain() ) );
298 163         19612 $header->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'header.i' )->safe_set_value( $signature->identity() ) );
299 163         19716 $header->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'header.b' )->safe_set_value( substr( $signature->data(), 0, 8 ) ) );
300 163 50 33     20584 $header->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'header.a' )->safe_set_value( $key_type . '-' . $hash_algorithm ) ) if ($key_type && $hash_algorithm);
301 163         16069 $header->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'header.s' )->safe_set_value( $selector ) );
302 163 100       14891 if ( $config->{'extra_properties'} ) {
303 12 50       73 $header->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'x-bits' )->safe_set_value( $key_size ) ) if $key_size;
304             }
305 163         2024 $self->add_auth_header($header);
306             }
307             }
308 163         721 $self->check_timeout();
309             }
310              
311             # the alleged author of the email may specify how to handle email
312 131 100 100     1752 if ( $config->{'check_adsp'}
      100        
      66        
313             && ( $self->is_local_ip_address() == 0 )
314             && ( $self->is_trusted_ip_address() == 0 )
315             && ( $self->is_authenticated() == 0 ) )
316             {
317             POLICY:
318 36         295 foreach my $policy ( $dkim->policies() ) {
319 108         106643 my $apply = $policy->apply($dkim);
320 108         28268 my $string = $policy->as_string();
321 108   100     2061 my $location = $policy->location() || q{};
322 108         1262 my $name = $policy->name();
323 108         835 my $default = $policy->is_implied_default_policy();
324              
325 108         1549 my $otype = ref $policy;
326 108 50       588 my $type =
    100          
    100          
327             $otype eq 'Mail::DKIM::AuthorDomainPolicy' ? 'dkim-adsp'
328             : $otype eq 'Mail::DKIM::DkimPolicy' ? 'x-dkim-ssp'
329             : $otype eq 'Mail::DKIM::DkPolicy' ? 'x-dkim-dkssp'
330             : 'x-dkim-policy';
331              
332 108         520 $self->dbgout( 'DKIMPolicy', $apply, LOG_DEBUG );
333 108         490 $self->dbgout( 'DKIMPolicyString', $string, LOG_DEBUG );
334 108         526 $self->dbgout( 'DKIMPolicyLocation', $location, LOG_DEBUG );
335 108         486 $self->dbgout( 'DKIMPolicyName', $name, LOG_DEBUG );
336 108 100       656 $self->dbgout( 'DKIMPolicyDefault', $default ? 'yes' : 'no', LOG_DEBUG );
337              
338 108 50 66     841 next POLICY if ( ( $type eq 'x-dkim-dkssp' ) && ( ! $self->show_domainkeys() ) );
339              
340 108 50       460 my $result =
    50          
    100          
341             $apply eq 'accept' ? 'pass'
342             : $apply eq 'reject' ? 'discard'
343             : $apply eq 'neutral' ? 'unknown'
344             : 'unknown';
345              
346 108 50 66     529 if ( ! ( $config->{'adsp_hide_none'} && $result eq 'none' ) ) {
347 108 100 66     669 if ( ( ! $default ) or $config->{'show_default_adsp'} ) {
348 36         292 my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( $type )->safe_set_value( $result );
349 36 50       2776 my $comment = ( $default ? 'default ' : q{} )
    50          
350             . "$name policy"
351             . ( $location ? " from $location" : q{} )
352             # . ( $string ? ", $string" : q{} )
353             ;
354 36         194 $header->add_child( Mail::AuthenticationResults::Header::Comment->new()->safe_set_value( $comment ) );
355 36         18349 $self->add_auth_header( $header );
356             }
357             }
358 108         405 $self->check_timeout();
359             }
360             }
361             };
362 131 50       972 if ( my $error = $@ ) {
363 0         0 $self->handle_exception( $error );
364              
365             # Also in DMARC module
366 0         0 $self->log_error( 'DKIM EOM Error ' . $error );
367 0         0 $self->{'failmode'} = 1;
368 0         0 $self->_check_error( $error );
369 0         0 $self->metric_count( 'dkim_total', { 'result' => 'error' } );
370 0         0 return;
371             }
372             }
373              
374             sub close_callback {
375 118     118 0 386 my ( $self ) = @_;
376 118         404 delete $self->{'failmode'};
377 118         317 delete $self->{'headers'};
378 118         283 delete $self->{'body'};
379 118         365 delete $self->{'carry'};
380 118         304 delete $self->{'has_dkim'};
381 118         360 delete $self->{'valid_domains'};
382 118         594 $self->destroy_object('dkim');
383             }
384              
385             sub _check_error {
386 0     0     my ( $self, $error ) = @_;
387 0 0 0       if ( $error =~ /^DNS error: query timed out/
    0 0        
    0 0        
      0        
388             or $error =~ /^DNS query timeout/
389             ){
390 0           $self->log_error( 'Temp DKIM Error - ' . $error );
391 0           my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( 'dkim' )->safe_set_value( 'temperror' );
392 0           $header->add_child( Mail::AuthenticationResults::Header::Comment->new()->safe_set_value( 'dns timeout' ) );
393 0           $self->add_auth_header( $header );
394             }
395             elsif ( $error =~ /^DNS error: SERVFAIL/ ){
396 0           $self->log_error( 'Temp DKIM Error - ' . $error );
397 0           my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( 'dkim' )->safe_set_value( 'temperror' );
398 0           $header->add_child( Mail::AuthenticationResults::Header::Comment->new()->safe_set_value( 'dns servfail' ) );
399 0           $self->add_auth_header( $header );
400             }
401             elsif ( $error =~ /^no domain to fetch policy for$/
402             or $error =~ /^policy syntax error$/
403             or $error =~ /^empty domain label/
404             or $error =~ /^invalid name /
405             ){
406 0           $self->log_error( 'Perm DKIM Error - ' . $error );
407 0           my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( 'dkim' )->safe_set_value( 'permerror' );
408 0           $header->add_child( Mail::AuthenticationResults::Header::Comment->new()->safe_set_value( 'syntax or domain error' ) );
409 0           $self->add_auth_header( $header );
410             }
411             else {
412 0           $self->exit_on_close( 'Unexpected DKIM Error - ' . $error );
413 0           my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( 'dkim' )->safe_set_value( 'temperror' );
414 0           $self->add_auth_header( $header );
415             # Fill these in as they occur, but for unknowns err on the side of caution
416             # and tempfail/exit
417 0           $self->tempfail_on_error();
418             }
419             }
420              
421             1;
422              
423             __END__
424              
425             =pod
426              
427             =encoding UTF-8
428              
429             =head1 NAME
430              
431             Mail::Milter::Authentication::Handler::DKIM - Handler class for DKIM
432              
433             =head1 VERSION
434              
435             version 3.20230911
436              
437             =head1 DESCRIPTION
438              
439             Module for validation of DKIM and DomainKeys signatures, and application of ADSP policies.
440              
441             =head1 CONFIGURATION
442              
443             "DKIM" : { | Config for the DKIM Module
444             "hide_none" : 0, | Hide auth line if the result is 'none'
445             "hide_domainkeys" : 0, | Hide any DomainKeys results
446             "check_adsp" : 1, | Also check for ADSP
447             "show_default_adsp" : 0, | Show the default ADSP result
448             "adsp_hide_none" : 0, | Hide auth ADSP if the result is 'none'
449             "extra_properties" : 0 | Add extra properties (not to rfc) relating to key and selector
450             "no_strict" : 0, | Ignore rfc 8301 security considerations (not recommended
451             },
452              
453             =head1 AUTHOR
454              
455             Marc Bradshaw <marc@marcbradshaw.net>
456              
457             =head1 COPYRIGHT AND LICENSE
458              
459             This software is copyright (c) 2020 by Marc Bradshaw.
460              
461             This is free software; you can redistribute it and/or modify it under
462             the same terms as the Perl 5 programming language system itself.
463              
464             =cut