File Coverage

blib/lib/Mail/DKIM/Verifier.pm
Criterion Covered Total %
statement 197 230 85.6
branch 65 92 70.6
condition 24 39 61.5
subroutine 21 25 84.0
pod 5 14 35.7
total 312 400 78.0


line stmt bran cond sub pod time code
1             package Mail::DKIM::Verifier;
2 5     5   403041 use strict;
  5         41  
  5         185  
3 5     5   34 use warnings;
  5         16  
  5         286  
4             our $VERSION = '1.20230911'; # VERSION
5             # ABSTRACT: verifies a DKIM-signed message
6              
7             # Copyright 2005-2009 Messiah College. All rights reserved.
8             # Jason Long
9              
10             # Copyright (c) 2004 Anthony D. Urso. All rights reserved.
11             # This program is free software; you can redistribute it and/or
12             # modify it under the same terms as Perl itself.
13              
14 5     5   2089 use Mail::DKIM::Signature;
  5         49  
  5         268  
15 5     5   2277 use Mail::DKIM::DkSignature;
  5         15  
  5         168  
16 5     5   1940 use Mail::Address;
  5         9933  
  5         163  
17              
18              
19              
20 5     5   33 use base 'Mail::DKIM::Common';
  5         11  
  5         1962  
21 5     5   34 use Carp;
  5         15  
  5         9885  
22             our $MAX_SIGNATURES_TO_PROCESS = 50;
23              
24             sub init {
25 82     82 0 131 my $self = shift;
26 82         281 $self->SUPER::init;
27 82         164 $self->{signatures} = [];
28             }
29              
30             # @{$dkim->{signatures}}
31             # array of L objects, representing all
32             # parseable signatures found in the header,
33             # ordered from the top of the header to the bottom.
34             #
35             # $dkim->{signature_reject_reason}
36             # simple string listing a reason, if any, for not using a signature.
37             # This may be a helpful diagnostic if there is a signature in the header,
38             # but was found not to be valid. It will be ambiguous if there are more
39             # than one signatures that could not be used.
40             #
41             # $dkim->{signature}
42             # the L selected as the "best" signature.
43             #
44             # @{$dkim->{headers}}
45             # array of strings, each member is one header, in its original format.
46             #
47             # $dkim->{algorithms}
48             # array of algorithms, one for each signature being verified.
49             #
50             # $dkim->{result}
51             # string; the result of the verification (see the result() method)
52             #
53              
54             sub handle_header {
55 701     701 0 978 my $self = shift;
56 701         1288 my ( $field_name, $contents, $line ) = @_;
57              
58 701         1786 $self->SUPER::handle_header( $field_name, $contents );
59              
60 701 100       1519 if ( lc($field_name) eq 'dkim-signature' ) {
61             eval {
62 70         230 local $SIG{__DIE__};
63 70         389 my $signature = Mail::DKIM::Signature->parse($line);
64 69         220 $self->add_signature($signature);
65 69         336 1
66 70 100       116 } || do {
67              
68             # the only reason an error should be thrown is if the
69             # signature really is unparse-able
70              
71             # otherwise, invalid signatures are caught in finish_header()
72              
73 1         3 chomp( my $E = $@ );
74 1         3 $self->{signature_reject_reason} = $E;
75             };
76             }
77              
78 701 100       1734 if ( lc($field_name) eq 'domainkey-signature' ) {
79             eval {
80 17         59 local $SIG{__DIE__};
81 17         101 my $signature = Mail::DKIM::DkSignature->parse($line);
82 17         65 $self->add_signature($signature);
83 17         103 1
84 17 50       31 } || do {
85              
86             # the only reason an error should be thrown is if the
87             # signature really is unparse-able
88              
89             # otherwise, invalid signatures are caught in finish_header()
90              
91 0         0 chomp( my $E = $@ );
92 0         0 $self->{signature_reject_reason} = $E;
93             };
94             }
95             }
96              
97             sub add_signature {
98 86     86 0 137 my $self = shift;
99 86 50       206 croak 'wrong number of arguments' unless ( @_ == 1 );
100 86         154 my ($signature) = @_;
101              
102             # ignore signature headers once we've seen 50 or so
103             # this protects against abuse.
104 86 50       127 return if ( @{ $self->{signatures} } > $MAX_SIGNATURES_TO_PROCESS );
  86         212  
105              
106 86         132 push @{ $self->{signatures} }, $signature;
  86         163  
107              
108 86 100       174 unless ( $self->check_signature($signature) ) {
109 13         58 $signature->result( 'invalid', $self->{signature_reject_reason} );
110 13         17 return;
111             }
112              
113             # signature looks ok, go ahead and query for the public key
114 73         203 $signature->fetch_public_key;
115              
116             # create a canonicalization filter and algorithm
117 73         240 my $algorithm_class =
118             $signature->get_algorithm_class( $signature->algorithm );
119             my $algorithm = $algorithm_class->new(
120             Signature => $signature,
121             Debug_Canonicalization => $self->{Debug_Canonicalization},
122 73         528 );
123              
124             # push through the headers parsed prior to the signature header
125 73 100       281 if ( $algorithm->wants_pre_signature_headers ) {
126              
127             # Note: this will include the signature header that led to this
128             # "algorithm"...
129 58         82 foreach my $head ( @{ $self->{headers} } ) {
  58         149  
130 67         121 $algorithm->add_header($head);
131             }
132             }
133              
134             # save the algorithm
135 73   50     179 $self->{algorithms} ||= [];
136 73         104 push @{ $self->{algorithms} }, $algorithm;
  73         178  
137             }
138              
139             sub check_signature {
140 86     86 0 123 my $self = shift;
141 86 50       244 croak 'wrong number of arguments' unless ( @_ == 1 );
142 86         143 my ($signature) = @_;
143              
144 86 100       231 unless ( $signature->check_version ) {
145              
146             # unsupported version
147 1 50       5 if ( defined $signature->version ) {
148             $self->{signature_reject_reason} =
149 1         3 'unsupported version ' . $signature->version;
150             }
151             else {
152 0         0 $self->{signature_reject_reason} = 'missing v tag';
153             }
154 1         4 return 0;
155             }
156              
157 85 100 66     211 unless ( $signature->algorithm
158             && $signature->get_algorithm_class( $signature->algorithm ) )
159             {
160             # unsupported algorithm
161 2         12 $self->{signature_reject_reason} = 'unsupported algorithm';
162 2 50       8 if ( defined $signature->algorithm ) {
163 2         7 $self->{signature_reject_reason} .= ' ' . $signature->algorithm;
164             }
165 2         7 return 0;
166             }
167              
168 83 100       234 if ( $self->{Strict} ) {
169 4 100       8 if ( $signature->algorithm eq 'rsa-sha1' ) {
170 2         6 $self->{signature_reject_reason} = 'unsupported algorithm';
171 2 50       4 if ( defined $signature->algorithm ) {
172 2         5 $self->{signature_reject_reason} .= ' ' . $signature->algorithm;
173             }
174 2         6 return 0;
175             }
176             }
177              
178 81 100       203 unless ( $signature->check_canonicalization ) {
179              
180             # unsupported canonicalization method
181 2         9 $self->{signature_reject_reason} = 'unsupported canonicalization';
182 2 50       7 if ( defined $signature->canonicalization ) {
183             $self->{signature_reject_reason} .=
184 2         7 ' ' . $signature->canonicalization;
185             }
186 2         7 return 0;
187             }
188              
189 79 100       187 unless ( $signature->check_protocol ) {
190              
191             # unsupported query protocol
192             $self->{signature_reject_reason} =
193 4 50       11 !defined( $signature->protocol )
194             ? 'missing q tag'
195             : 'unsupported query protocol, q=' . $signature->protocol;
196 4         17 return 0;
197             }
198              
199 75 100       201 unless ( $signature->check_expiration ) {
200              
201             # signature has expired
202 2         9 $self->{signature_reject_reason} = 'signature is expired';
203 2         7 return 0;
204             }
205              
206 73 50       182 unless ( defined $signature->domain ) {
207              
208             # no domain specified
209 0         0 $self->{signature_reject_reason} = 'missing d tag';
210 0         0 return 0;
211             }
212              
213 73 50       160 if ( $signature->domain eq '' ) {
214              
215             # blank domain
216 0         0 $self->{signature_reject_reason} = 'invalid domain in d tag';
217 0         0 return 0;
218             }
219              
220 73 50       180 unless ( defined $signature->selector ) {
221              
222             # no selector specified
223 0         0 $self->{signature_reject_reason} = 'missing s tag';
224 0         0 return 0;
225             }
226              
227 73         186 return 1;
228             }
229              
230             sub check_public_key {
231 59     59 0 102 my $self = shift;
232 59 50       124 croak 'wrong number of arguments' unless ( @_ == 2 );
233 59         112 my ( $signature, $public_key ) = @_;
234              
235 59         83 my $result = 0;
236             eval {
237 59         189 local $SIG{__DIE__};
238 59         97 $@ = undef;
239              
240             # HACK- I'm indecisive here about whether I want the
241             # check_foo functions to return false or to "die"
242             # on failure
243              
244             # check public key's allowed hash algorithms
245 59         153 $result =
246             $public_key->check_hash_algorithm( $signature->hash_algorithm );
247              
248             # HACK- DomainKeys signatures are allowed to have an empty g=
249             # tag in the public key
250 57         376 my $empty_g_means_wildcard = $signature->isa('Mail::DKIM::DkSignature');
251              
252             # check public key's granularity
253 57   66     241 $result &&=
254             $public_key->check_granularity( $signature->identity,
255             $empty_g_means_wildcard );
256              
257 57 100       139 die $@ if $@;
258 52         191 1
259 59 100       92 } || do {
260 7         13 my $E = $@;
261 7         14 chomp $E;
262 7         28 $self->{signature_reject_reason} = "public key: $E";
263             };
264 59         159 return $result;
265             }
266              
267             # returns true if the i= tag is an address with a domain matching or
268             # a subdomain of the d= tag
269             #
270             sub check_signature_identity {
271 73     73 0 140 my ($signature) = @_;
272              
273 73         171 my $d = $signature->domain;
274 73         240 my $i = $signature->identity;
275 73 50 33     535 if ( defined($i) && $i =~ /\@([^@]*)$/ ) {
276 73         179 return match_subdomain( $1, $d );
277             }
278 0         0 return 0;
279             }
280              
281             sub match_subdomain {
282 73 50   73 0 167 croak 'wrong number of arguments' unless ( @_ == 2 );
283 73         209 my ( $subdomain, $superdomain ) = @_;
284              
285 73         223 my $tmp = substr( ".$subdomain", -1 - length($superdomain) );
286 73         297 return ( lc ".$superdomain" eq lc $tmp );
287             }
288              
289             #
290             # called when the verifier has received the last of the message headers
291             # (body is still to come)
292             #
293             sub finish_header {
294 81     81 0 133 my $self = shift;
295              
296             # Signatures we found and were successfully parsed are stored in
297             # $self->{signatures}. If none were found, our result is "none".
298              
299 81 100 66     120 if ( @{ $self->{signatures} } == 0
  81         270  
300             && !defined( $self->{signature_reject_reason} ) )
301             {
302 1         4 $self->{result} = 'none';
303 1         3 return;
304             }
305              
306 80         160 foreach my $algorithm ( @{ $self->{algorithms} } ) {
  80         142  
307 73         253 $algorithm->finish_header( Headers => $self->{headers} );
308             }
309              
310             # stop processing signatures that are already known to be invalid
311 80         206 @{ $self->{algorithms} } = grep {
312 73         205 my $sig = $_->signature;
313 73   33     176 !( $sig->result && $sig->result eq 'invalid' );
314 80         133 } @{ $self->{algorithms} };
  80         190  
315              
316 80 100 66     113 if ( @{ $self->{algorithms} } == 0
  80         278  
317 12         48 && @{ $self->{signatures} } > 0 )
318             {
319 12   50     38 $self->{result} = $self->{signatures}->[0]->result || 'invalid';
320             $self->{details} = $self->{signatures}->[0]->{verify_details}
321 12   33     44 || $self->{signature_reject_reason};
322 12         34 return;
323             }
324             }
325              
326             sub _check_and_verify_signature {
327 73     73   131 my $self = shift;
328 73         136 my ($algorithm) = @_;
329              
330             # check signature
331 73         177 my $signature = $algorithm->signature;
332 73 100       164 unless ( check_signature_identity($signature) ) {
333 3         14 $self->{signature_reject_reason} = 'bad identity';
334 3         9 return ( 'invalid', $self->{signature_reject_reason} );
335             }
336              
337             # get public key
338 70         130 my $pkey;
339 70         201 eval { $pkey = $signature->get_public_key; 1 }
  59         147  
340 70 100       113 || do {
341 11         26 my $E = $@;
342 11         19 chomp $E;
343 11         45 $self->{signature_reject_reason} = "public key: $E";
344 11         37 return ( 'invalid', $self->{signature_reject_reason} );
345             };
346              
347 59 100       152 unless ( $self->check_public_key( $signature, $pkey ) ) {
348 7         24 return ( 'invalid', $self->{signature_reject_reason} );
349             }
350              
351             # special handling for RSA signatures
352 52   100     140 my $k = $pkey->get_tag('k') || 'rsa';
353 52 100       134 if ($k eq 'rsa') {
354             # make sure key is big enough
355 49         124 my $keysize = $pkey->cork->size * 8; # in bits
356 49 100 100     168 if ( $keysize < 1024 && $self->{Strict} ) {
357 1         10 $self->{signature_reject_reason} = "Key length $keysize too short";
358 1         10 return ( 'fail', $self->{signature_reject_reason} );
359             }
360             }
361              
362             # verify signature
363 51         82 my $result;
364             my $details;
365 51         83 local $@ = undef;
366             eval {
367 51 100       178 $result = $algorithm->verify() ? 'pass' : 'fail';
368 49   100     196 $details = $algorithm->{verification_details} || $@;
369 49         116 1
370 51 100       86 } || do {
371              
372             # see also add_signature
373 2         8 chomp( my $E = $@ );
374 2 50       19 if ( $E =~ /(OpenSSL error: .*?) at / ) {
    0          
    0          
375 2         7 $E = $1;
376             }
377             elsif ( $E =~ /^(panic:.*?) at / ) {
378 0         0 $E = "OpenSSL $1";
379             }
380             elsif ( $E =~ /^FATAL: (.*) at / ) {
381 0         0 $E = "Ed25519 $1";
382             }
383 2         4 $result = 'fail';
384 2         4 $details = $E;
385             };
386 51         165 return ( $result, $details );
387             }
388              
389             sub finish_body {
390 81     81 0 130 my $self = shift;
391              
392 81         138 foreach my $algorithm ( @{ $self->{algorithms} } ) {
  81         171  
393              
394             # finish canonicalizing
395 73         212 $algorithm->finish_body;
396              
397 73         214 my ( $result, $details ) =
398             $self->_check_and_verify_signature($algorithm);
399              
400             # save the results of this signature verification
401 73         184 $algorithm->{result} = $result;
402 73         150 $algorithm->{details} = $details;
403 73         226 $algorithm->signature->result( $result, $details );
404              
405             # collate results ... ignore failed signatures if we already got
406             # one to pass
407 73 100 100     219 if ( !$self->{result} || $result eq 'pass' ) {
408 70         153 $self->{signature} = $algorithm->signature;
409 70         131 $self->{result} = $result;
410 70         244 $self->{details} = $details;
411             }
412             }
413             }
414              
415              
416             sub fetch_author_domain_policies {
417 0     0 1 0 my $self = shift;
418 5     5   2378 use Mail::DKIM::AuthorDomainPolicy;
  5         13  
  5         642  
419              
420 0 0       0 return () unless $self->{headers_by_name}->{from};
421 0         0 my @list = Mail::Address->parse( $self->{headers_by_name}->{from} );
422 0         0 my @authors = map { $_->address } @list;
  0         0  
423              
424             # fetch the policies
425             return map {
426 0         0 Mail::DKIM::AuthorDomainPolicy->fetch(
  0         0  
427             Protocol => 'dns',
428             Author => $_,
429             )
430             } @authors;
431             }
432              
433              
434             sub fetch_author_policy {
435 0     0 1 0 my $self = shift;
436 0         0 my ($author) = @_;
437 5     5   2313 use Mail::DKIM::DkimPolicy;
  5         13  
  5         389  
438              
439             # determine address found in the "From"
440 0   0     0 $author ||= $self->message_originator->address;
441              
442             # fetch the policy
443 0         0 return Mail::DKIM::DkimPolicy->fetch(
444             Protocol => 'dns',
445             Author => $author,
446             );
447             }
448              
449              
450             sub fetch_sender_policy {
451 0     0 1 0 my $self = shift;
452 5     5   2258 use Mail::DKIM::DkPolicy;
  5         12  
  5         839  
453              
454             # determine addresses found in the "From" and "Sender" headers
455 0         0 my $author = $self->message_originator->address;
456 0         0 my $sender = $self->message_sender->address;
457              
458             # fetch the policy
459 0         0 return Mail::DKIM::DkPolicy->fetch(
460             Protocol => 'dns',
461             Author => $author,
462             Sender => $sender,
463             );
464             }
465              
466              
467             sub policies {
468 0     0 1 0 my $self = shift;
469              
470 0         0 my $sender_policy = eval { $self->fetch_sender_policy() };
  0         0  
471 0         0 my $author_policy = eval { $self->fetch_author_policy() };
  0         0  
472             return (
473 0 0       0 $sender_policy ? $sender_policy : (),
    0          
474             $author_policy ? $author_policy : (),
475             $self->fetch_author_domain_policies(),
476             );
477             }
478              
479              
480              
481              
482             sub signatures {
483 4     4 1 727 my $self = shift;
484 4 50       15 croak 'unexpected argument' if @_;
485              
486 4         7 return @{ $self->{signatures} };
  4         31  
487             }
488              
489             1;
490              
491             __END__